mirror of
https://github.com/esphome/esphome.git
synced 2025-11-15 14:25:45 +00:00
Compare commits
1 Commits
integratio
...
de_dupe_la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc1b547ad2 |
@@ -497,6 +497,11 @@ def generate_cpp_contents(config: ConfigType) -> None:
|
||||
|
||||
CORE.flush_tasks()
|
||||
|
||||
# Flush deferred lambda deduplication declarations after all variables are declared
|
||||
from esphome import cpp_generator as cg
|
||||
|
||||
cg.flush_lambda_dedup_declarations()
|
||||
|
||||
|
||||
def write_cpp_file() -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
@@ -741,13 +746,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 +1207,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",
|
||||
|
||||
@@ -15,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
|
||||
@@ -196,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)
|
||||
@@ -280,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)")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -389,8 +389,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (this->conn_id_ != param->search_res.conn_id)
|
||||
return false;
|
||||
this->service_count_++;
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// V3 clients don't need services initialized since
|
||||
// as they use the ESP APIs to get services.
|
||||
break;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,6 @@ void LightState::setup() {
|
||||
effect->init_internal(this);
|
||||
}
|
||||
|
||||
// Start with loop disabled if idle - respects any effects/transitions set up during initialization
|
||||
this->disable_loop_if_idle_();
|
||||
|
||||
// When supported color temperature range is known, initialize color temperature setting within bounds.
|
||||
auto traits = this->get_traits();
|
||||
float min_mireds = traits.get_min_mireds();
|
||||
@@ -129,9 +126,6 @@ void LightState::loop() {
|
||||
this->is_transformer_active_ = false;
|
||||
this->transformer_ = nullptr;
|
||||
this->target_state_reached_callback_.call();
|
||||
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +133,6 @@ void LightState::loop() {
|
||||
if (this->next_write_) {
|
||||
this->next_write_ = false;
|
||||
this->output_->write_state(this);
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +228,6 @@ void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->active_effect_index_ = effect_index;
|
||||
auto *effect = this->get_active_effect_();
|
||||
effect->start_internal();
|
||||
// Enable loop while effect is active
|
||||
this->enable_loop();
|
||||
}
|
||||
LightEffect *LightState::get_active_effect_() {
|
||||
if (this->active_effect_index_ == 0) {
|
||||
@@ -252,8 +242,6 @@ void LightState::stop_effect_() {
|
||||
effect->stop();
|
||||
}
|
||||
this->active_effect_index_ = 0;
|
||||
// Disable loop if idle (no effect and no transformer)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
|
||||
void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -263,8 +251,6 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
}
|
||||
// Enable loop while transition is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -280,8 +266,6 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
};
|
||||
// Enable loop while flash is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
||||
@@ -293,14 +277,6 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
|
||||
}
|
||||
this->output_->update_state(this);
|
||||
this->next_write_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::disable_loop_if_idle_() {
|
||||
// Only disable loop if both transformer and effect are inactive, and no pending writes
|
||||
if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LightState::save_remote_values_() {
|
||||
|
||||
@@ -256,9 +256,6 @@ class LightState : public EntityBase, public Component {
|
||||
/// Internal method to save the current remote_values to the preferences
|
||||
void save_remote_values_();
|
||||
|
||||
/// Disable loop if neither transformer nor effect is active
|
||||
void disable_loop_if_idle_();
|
||||
|
||||
/// Store the output to allow effects to have more access.
|
||||
LightOutput *output_;
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -174,9 +174,6 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
|
||||
# OpenThread uses esp_vfs_eventfd which requires VFS select support
|
||||
require_vfs_select()
|
||||
|
||||
# OpenThread SRP needs access to mDNS services after setup
|
||||
enable_mdns_storage()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,69 +1,17 @@
|
||||
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
|
||||
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"]
|
||||
|
||||
CONF_WEB_SERVER = "web_server"
|
||||
|
||||
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:
|
||||
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(
|
||||
{
|
||||
@@ -74,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):
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == '/')); }
|
||||
|
||||
@@ -145,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]; }
|
||||
|
||||
@@ -872,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:
|
||||
|
||||
@@ -24,6 +24,10 @@ 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"
|
||||
|
||||
|
||||
class RawExpression(Expression):
|
||||
__slots__ = ("text",)
|
||||
@@ -188,7 +192,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)
|
||||
@@ -197,16 +201,21 @@ class LambdaExpression(Expression):
|
||||
self.capture = capture
|
||||
self.return_type = safe_exp(return_type) if return_type is not None else None
|
||||
|
||||
def __str__(self):
|
||||
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:
|
||||
# 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 += " {\n"
|
||||
if self.source is not None:
|
||||
cpp += f"{self.source.as_line_directive}\n"
|
||||
cpp += f"{self.content}\n}}"
|
||||
cpp += f" {{\n{self._format_body()}\n}}"
|
||||
return indent_all_but_first_and_last(cpp)
|
||||
|
||||
@property
|
||||
@@ -214,6 +223,37 @@ 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__ = ()
|
||||
@@ -583,6 +623,24 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
|
||||
CORE.add_global(expression, prepend)
|
||||
|
||||
|
||||
def flush_lambda_dedup_declarations():
|
||||
"""Flush all deferred lambda deduplication declarations to global scope.
|
||||
|
||||
This must be called after all component code generation is complete
|
||||
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.
|
||||
|
||||
@@ -656,6 +714,62 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
|
||||
return await CORE.get_variable_with_full_id(id_)
|
||||
|
||||
|
||||
def _try_deduplicate_lambda(lambda_expr: LambdaExpression) -> str | None:
|
||||
"""Try to deduplicate a lambda expression.
|
||||
|
||||
If an identical lambda was already generated, returns the name of the
|
||||
shared function. Otherwise, creates a new shared function and stores it.
|
||||
|
||||
Args:
|
||||
lambda_expr: The lambda expression to potentially deduplicate
|
||||
|
||||
Returns:
|
||||
The name of the shared function if this lambda should be deduplicated,
|
||||
None if this is the first occurrence (caller should use original lambda)
|
||||
"""
|
||||
# Create a unique key from the lambda content, parameters, and return type
|
||||
content = lambda_expr.content
|
||||
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] = {}
|
||||
|
||||
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
|
||||
if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data:
|
||||
CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = []
|
||||
CORE.data[_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,
|
||||
@@ -713,6 +827,18 @@ 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.
|
||||
if capture == "":
|
||||
lambda_expr = LambdaExpression(
|
||||
parts, parameters, capture, return_type, location
|
||||
)
|
||||
func_name = _try_deduplicate_lambda(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)
|
||||
|
||||
|
||||
|
||||
@@ -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,73 +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 import MemoryAnalyzer
|
||||
|
||||
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
|
||||
from esphome.analyze_memory import get_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 = MemoryAnalyzer(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)
|
||||
|
||||
@@ -1,21 +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 (
|
||||
CONF_WEB_SERVER,
|
||||
_web_server_ota_final_validate,
|
||||
)
|
||||
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM
|
||||
from esphome.core import ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
|
||||
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
|
||||
@@ -115,111 +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_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)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Tests for SNTP component."""
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -66,6 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
# Stateless lambda optimization: empty capture list allows function pointer conversion
|
||||
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
|
||||
assert 'return std::string{"Hello"};' in main_cpp
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
esphome:
|
||||
name: template-alarm-many-sensors
|
||||
friendly_name: "Template Alarm Control Panel with Many Sensors"
|
||||
|
||||
logger:
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: sensor1
|
||||
name: "Door 1"
|
||||
- platform: template
|
||||
id: sensor2
|
||||
name: "Door 2"
|
||||
- platform: template
|
||||
id: sensor3
|
||||
name: "Window 1"
|
||||
- platform: template
|
||||
id: sensor4
|
||||
name: "Window 2"
|
||||
- platform: template
|
||||
id: sensor5
|
||||
name: "Motion 1"
|
||||
- platform: template
|
||||
id: sensor6
|
||||
name: "Motion 2"
|
||||
- platform: template
|
||||
id: sensor7
|
||||
name: "Glass Break 1"
|
||||
- platform: template
|
||||
id: sensor8
|
||||
name: "Glass Break 2"
|
||||
- platform: template
|
||||
id: sensor9
|
||||
name: "Smoke Detector"
|
||||
- platform: template
|
||||
id: sensor10
|
||||
name: "CO Detector"
|
||||
|
||||
alarm_control_panel:
|
||||
- platform: template
|
||||
id: test_alarm
|
||||
name: "Test Alarm"
|
||||
codes:
|
||||
- "1234"
|
||||
requires_code_to_arm: true
|
||||
arming_away_time: 5s
|
||||
arming_home_time: 3s
|
||||
arming_night_time: 3s
|
||||
pending_time: 10s
|
||||
trigger_time: 300s
|
||||
restore_mode: ALWAYS_DISARMED
|
||||
binary_sensors:
|
||||
- input: sensor1
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor2
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: true
|
||||
chime: true
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor3
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor4
|
||||
bypass_armed_home: true
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: DELAYED
|
||||
- input: sensor5
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor6
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: true
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor7
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor8
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT
|
||||
- input: sensor9
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
- input: sensor10
|
||||
bypass_armed_home: false
|
||||
bypass_armed_night: false
|
||||
bypass_auto: false
|
||||
chime: false
|
||||
trigger_mode: INSTANT_ALWAYS
|
||||
on_disarmed:
|
||||
- logger.log: "Alarm disarmed"
|
||||
on_arming:
|
||||
- logger.log: "Alarm arming"
|
||||
on_armed_away:
|
||||
- logger.log: "Alarm armed away"
|
||||
on_armed_home:
|
||||
- logger.log: "Alarm armed home"
|
||||
on_armed_night:
|
||||
- logger.log: "Alarm armed night"
|
||||
on_pending:
|
||||
- logger.log: "Alarm pending"
|
||||
on_triggered:
|
||||
- logger.log: "Alarm triggered"
|
||||
on_cleared:
|
||||
- logger.log: "Alarm cleared"
|
||||
on_chime:
|
||||
- logger.log: "Chime activated"
|
||||
on_ready:
|
||||
- logger.log: "Sensors ready state changed"
|
||||
@@ -1,118 +0,0 @@
|
||||
"""Integration test for template alarm control panel with many sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi.model import APIIntEnum
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
class EspHomeACPFeatures(APIIntEnum):
|
||||
"""ESPHome AlarmControlPanel feature numbers."""
|
||||
|
||||
ARM_HOME = 1
|
||||
ARM_AWAY = 2
|
||||
ARM_NIGHT = 4
|
||||
TRIGGER = 8
|
||||
ARM_CUSTOM_BYPASS = 16
|
||||
ARM_VACATION = 32
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_template_alarm_control_panel_many_sensors(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test template alarm control panel with 10 binary sensors using FixedVector."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get entity info first
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Find the alarm control panel and binary sensors
|
||||
alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None
|
||||
binary_sensors: list[aioesphomeapi.BinarySensorInfo] = []
|
||||
|
||||
for entity in entities:
|
||||
if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo):
|
||||
alarm_info = entity
|
||||
elif isinstance(entity, aioesphomeapi.BinarySensorInfo):
|
||||
binary_sensors.append(entity)
|
||||
|
||||
assert alarm_info is not None, "Alarm control panel entity info not found"
|
||||
assert alarm_info.name == "Test Alarm"
|
||||
assert alarm_info.requires_code is True
|
||||
assert alarm_info.requires_code_to_arm is True
|
||||
|
||||
# Verify we have 10 binary sensors
|
||||
assert len(binary_sensors) == 10, (
|
||||
f"Expected 10 binary sensors, got {len(binary_sensors)}"
|
||||
)
|
||||
|
||||
# Verify sensor names
|
||||
expected_sensor_names = {
|
||||
"Door 1",
|
||||
"Door 2",
|
||||
"Window 1",
|
||||
"Window 2",
|
||||
"Motion 1",
|
||||
"Motion 2",
|
||||
"Glass Break 1",
|
||||
"Glass Break 2",
|
||||
"Smoke Detector",
|
||||
"CO Detector",
|
||||
}
|
||||
actual_sensor_names = {sensor.name for sensor in binary_sensors}
|
||||
assert actual_sensor_names == expected_sensor_names, (
|
||||
f"Sensor names mismatch. Expected: {expected_sensor_names}, "
|
||||
f"Got: {actual_sensor_names}"
|
||||
)
|
||||
|
||||
# Use InitialStateHelper to wait for all initial states
|
||||
state_helper = InitialStateHelper(entities)
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
# We'll receive subsequent states here after initial states
|
||||
pass
|
||||
|
||||
client.subscribe_states(state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states
|
||||
await state_helper.wait_for_initial_states(timeout=5.0)
|
||||
|
||||
# Verify the alarm state is disarmed initially
|
||||
alarm_state = state_helper.initial_states.get(alarm_info.key)
|
||||
assert alarm_state is not None, "Alarm control panel initial state not received"
|
||||
assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState)
|
||||
assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, (
|
||||
f"Expected initial state DISARMED, got {alarm_state.state}"
|
||||
)
|
||||
|
||||
# Verify all 10 binary sensors have initial states
|
||||
binary_sensor_states = [
|
||||
state_helper.initial_states.get(sensor.key) for sensor in binary_sensors
|
||||
]
|
||||
assert all(state is not None for state in binary_sensor_states), (
|
||||
"Not all binary sensors have initial states"
|
||||
)
|
||||
|
||||
# Verify all binary sensor states are BinarySensorState type
|
||||
for i, state in enumerate(binary_sensor_states):
|
||||
assert isinstance(state, aioesphomeapi.BinarySensorState), (
|
||||
f"Binary sensor {i} state is not BinarySensorState: {type(state)}"
|
||||
)
|
||||
|
||||
# Verify supported features
|
||||
expected_features = (
|
||||
EspHomeACPFeatures.ARM_HOME
|
||||
| EspHomeACPFeatures.ARM_AWAY
|
||||
| EspHomeACPFeatures.ARM_NIGHT
|
||||
| EspHomeACPFeatures.TRIGGER
|
||||
)
|
||||
assert alarm_info.supported_features == expected_features, (
|
||||
f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), "
|
||||
f"got {alarm_info.supported_features}"
|
||||
)
|
||||
@@ -355,7 +355,6 @@ def test_clean_build(
|
||||
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
|
||||
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
|
||||
mock_core.relative_build_path.return_value = dependencies_lock
|
||||
mock_core.platformio_cache_dir = str(platformio_cache_dir)
|
||||
|
||||
# Verify all exist before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
Reference in New Issue
Block a user