mirror of
https://github.com/esphome/esphome.git
synced 2025-10-30 14:43:51 +00:00
Merge remote-tracking branch 'upstream/dev' into component_source_logstring
This commit is contained in:
@@ -396,7 +396,10 @@ def check_permissions(port: str):
|
||||
)
|
||||
|
||||
|
||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
|
||||
def upload_program(
|
||||
config: ConfigType, args: ArgsProtocol, devices: list[str]
|
||||
) -> int | str:
|
||||
host = devices[0]
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "upload_program")(config, args, host):
|
||||
@@ -433,10 +436,10 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
|
||||
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD, "")
|
||||
binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
|
||||
|
||||
# Check if we should use MQTT for address resolution
|
||||
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
|
||||
devices: list[str] = args.device or []
|
||||
if (
|
||||
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
|
||||
and (not devices or host in ("MQTT", "OTA"))
|
||||
@@ -447,14 +450,13 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
|
||||
):
|
||||
from esphome import mqtt
|
||||
|
||||
host = mqtt.get_esphome_device_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)
|
||||
devices = [
|
||||
mqtt.get_esphome_device_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)
|
||||
]
|
||||
|
||||
if getattr(args, "file", None) is not None:
|
||||
return espota2.run_ota(host, remote_port, password, args.file)
|
||||
|
||||
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
||||
return espota2.run_ota(devices, remote_port, password, binary)
|
||||
|
||||
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
@@ -551,17 +553,11 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
purpose="uploading",
|
||||
)
|
||||
|
||||
# Try each device until one succeeds
|
||||
exit_code = 1
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
exit_code = upload_program(config, args, devices)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
else:
|
||||
_LOGGER.warning("Failed to upload to %s", devices)
|
||||
return exit_code
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_CURRENT_AC,
|
||||
ICON_LIGHTBULB,
|
||||
@@ -78,6 +79,7 @@ CONFIG_SCHEMA = (
|
||||
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_REACTIVE_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
|
||||
|
||||
@@ -17,10 +17,12 @@ from esphome.const import (
|
||||
CONF_REACTIVE_POWER,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
@@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
|
||||
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
device_class=DEVICE_CLASS_REACTIVE_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT_AMPS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
device_class=DEVICE_CLASS_APPARENT_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
|
||||
|
||||
@@ -493,7 +493,7 @@ void BedJetHub::dump_config() {
|
||||
" ble_client.app_id: %d\n"
|
||||
" ble_client.conn_id: %d",
|
||||
this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id());
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size());
|
||||
for (auto *child : this->children_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str());
|
||||
|
||||
@@ -152,7 +152,7 @@ void CCS811Component::send_env_data_() {
|
||||
void CCS811Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CCS811");
|
||||
LOG_I2C_DEVICE(this)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "CO2 Sensor", this->co2_);
|
||||
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_);
|
||||
LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_OTA
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::gpio_expander {
|
||||
@@ -11,18 +12,27 @@ namespace esphome::gpio_expander {
|
||||
/// @brief A class to cache the read state of a GPIO expander.
|
||||
/// This class caches reads between GPIO Pins which are on the same bank.
|
||||
/// This means that for reading whole Port (ex. 8 pins) component needs only one
|
||||
/// I2C/SPI read per main loop call. It assumes, that one bit in byte identifies one GPIO pin
|
||||
/// I2C/SPI read per main loop call. It assumes that one bit in byte identifies one GPIO pin.
|
||||
///
|
||||
/// Template parameters:
|
||||
/// T - Type which represents internal register. Could be uint8_t or uint16_t. Adjust to
|
||||
/// match size of your internal GPIO bank register.
|
||||
/// N - Number of pins
|
||||
template<typename T, T N> class CachedGpioExpander {
|
||||
/// T - Type which represents internal bank register. Could be uint8_t or uint16_t.
|
||||
/// Choose based on how your I/O expander reads pins:
|
||||
/// * uint8_t: For chips that read banks separately (8 pins at a time)
|
||||
/// Examples: MCP23017 (2x8-bit banks), TCA9555 (2x8-bit banks)
|
||||
/// * uint16_t: For chips that read all pins at once (up to 16 pins)
|
||||
/// Examples: PCF8574/8575 (8/16 pins), PCA9554/9555 (8/16 pins)
|
||||
/// N - Total number of pins (maximum 65535)
|
||||
/// P - Type for pin number parameters (automatically selected based on N:
|
||||
/// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified
|
||||
/// if needed (e.g., for components like SN74HC165 with >256 pins)
|
||||
template<typename T, uint16_t N, typename P = typename std::conditional<(N > 256), uint16_t, uint8_t>::type>
|
||||
class CachedGpioExpander {
|
||||
public:
|
||||
/// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
|
||||
/// @param pin Pin number to read
|
||||
/// @return Pin state
|
||||
bool digital_read(T pin) {
|
||||
const uint8_t bank = pin / BANK_SIZE;
|
||||
bool digital_read(P pin) {
|
||||
const P bank = pin / BANK_SIZE;
|
||||
const T pin_mask = (1 << (pin % BANK_SIZE));
|
||||
// Check if specific pin cache is valid
|
||||
if (this->read_cache_valid_[bank] & pin_mask) {
|
||||
@@ -38,21 +48,31 @@ template<typename T, T N> class CachedGpioExpander {
|
||||
return this->digital_read_cache(pin);
|
||||
}
|
||||
|
||||
void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); }
|
||||
void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); }
|
||||
|
||||
protected:
|
||||
/// @brief Call component low level function to read GPIO state from device
|
||||
virtual bool digital_read_hw(T pin) = 0;
|
||||
/// @brief Call component read function from internal cache.
|
||||
virtual bool digital_read_cache(T pin) = 0;
|
||||
/// @brief Call component low level function to write GPIO state to device
|
||||
virtual void digital_write_hw(T pin, bool value) = 0;
|
||||
/// @brief Read GPIO bank from hardware into internal state
|
||||
/// @param pin Pin number (used to determine which bank to read)
|
||||
/// @return true if read succeeded, false on communication error
|
||||
/// @note This does NOT return the pin state. It returns whether the read operation succeeded.
|
||||
/// The actual pin state should be returned by digital_read_cache().
|
||||
virtual bool digital_read_hw(P pin) = 0;
|
||||
|
||||
/// @brief Get cached pin value from internal state
|
||||
/// @param pin Pin number to read
|
||||
/// @return Pin state (true = HIGH, false = LOW)
|
||||
virtual bool digital_read_cache(P pin) = 0;
|
||||
|
||||
/// @brief Write GPIO state to hardware
|
||||
/// @param pin Pin number to write
|
||||
/// @param value Pin state to write (true = HIGH, false = LOW)
|
||||
virtual void digital_write_hw(P pin, bool value) = 0;
|
||||
|
||||
/// @brief Invalidate cache. This function should be called in component loop().
|
||||
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
|
||||
|
||||
static constexpr uint8_t BITS_PER_BYTE = 8;
|
||||
static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
|
||||
static constexpr uint16_t BITS_PER_BYTE = 8;
|
||||
static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
|
||||
static constexpr size_t BANKS = N / BANK_SIZE;
|
||||
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ void GroveGasMultichannelV2Component::update() {
|
||||
void GroveGasMultichannelV2Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2");
|
||||
LOG_I2C_DEVICE(this)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_);
|
||||
LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_);
|
||||
LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_);
|
||||
|
||||
@@ -42,7 +42,7 @@ void HLW8012Component::dump_config() {
|
||||
" Current resistor: %.1f mΩ\n"
|
||||
" Voltage Divider: %.1f",
|
||||
this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_);
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
|
||||
@@ -246,14 +246,35 @@ void Logger::add_on_log_callback(std::function<void(uint8_t, const char *, const
|
||||
this->log_callback_.add(std::move(callback));
|
||||
}
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// ESP8266: PSTR() cannot be used in array initializers, so we need to declare
|
||||
// each string separately as a global constant first
|
||||
static const char LOG_LEVEL_NONE[] PROGMEM = "NONE";
|
||||
static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR";
|
||||
static const char LOG_LEVEL_WARN[] PROGMEM = "WARN";
|
||||
static const char LOG_LEVEL_INFO[] PROGMEM = "INFO";
|
||||
static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG";
|
||||
static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG";
|
||||
static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE";
|
||||
static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE";
|
||||
|
||||
static const LogString *const LOG_LEVELS[] = {
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_NONE), reinterpret_cast<const LogString *>(LOG_LEVEL_ERROR),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_WARN), reinterpret_cast<const LogString *>(LOG_LEVEL_INFO),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_CONFIG), reinterpret_cast<const LogString *>(LOG_LEVEL_DEBUG),
|
||||
reinterpret_cast<const LogString *>(LOG_LEVEL_VERBOSE), reinterpret_cast<const LogString *>(LOG_LEVEL_VERY_VERBOSE),
|
||||
};
|
||||
#else
|
||||
static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#endif
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Logger:\n"
|
||||
" Max Level: %s\n"
|
||||
" Initial Level: %s",
|
||||
LOG_LEVELS[ESPHOME_LOG_LEVEL], LOG_LEVELS[this->current_level_]);
|
||||
LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_]));
|
||||
#ifndef USE_HOST
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Log Baud Rate: %" PRIu32 "\n"
|
||||
@@ -267,14 +288,14 @@ void Logger::dump_config() {
|
||||
#endif
|
||||
|
||||
for (auto &it : this->log_levels_) {
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]);
|
||||
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second]));
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::set_log_level(uint8_t level) {
|
||||
if (level > ESPHOME_LOG_LEVEL) {
|
||||
level = ESPHOME_LOG_LEVEL;
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
|
||||
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]));
|
||||
}
|
||||
this->current_level_ = level;
|
||||
this->level_callback_.call(level);
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_OUTPUT,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
@@ -22,14 +22,29 @@ void MCP23016::setup() {
|
||||
this->write_reg_(MCP23016_IODIR0, 0xFF);
|
||||
this->write_reg_(MCP23016_IODIR1, 0xFF);
|
||||
}
|
||||
bool MCP23016::digital_read(uint8_t pin) {
|
||||
uint8_t bit = pin % 8;
|
||||
|
||||
void MCP23016::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
}
|
||||
bool MCP23016::digital_read_hw(uint8_t pin) {
|
||||
uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1;
|
||||
uint8_t value = 0;
|
||||
this->read_reg_(reg_addr, &value);
|
||||
return value & (1 << bit);
|
||||
if (!this->read_reg_(reg_addr, &value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the appropriate part of input_mask_
|
||||
if (pin < 8) {
|
||||
this->input_mask_ = (this->input_mask_ & 0xFF00) | value;
|
||||
} else {
|
||||
this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void MCP23016::digital_write(uint8_t pin, bool value) {
|
||||
|
||||
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
void MCP23016::digital_write_hw(uint8_t pin, bool value) {
|
||||
uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1;
|
||||
this->update_reg_(pin, value, reg_addr);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mcp23016 {
|
||||
@@ -24,19 +25,22 @@ enum MCP23016GPIORegisters {
|
||||
MCP23016_IOCON1 = 0x0B,
|
||||
};
|
||||
|
||||
class MCP23016 : public Component, public i2c::I2CDevice {
|
||||
class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint8_t, 16> {
|
||||
public:
|
||||
MCP23016() = default;
|
||||
|
||||
void setup() override;
|
||||
|
||||
bool digital_read(uint8_t pin);
|
||||
void digital_write(uint8_t pin, bool value);
|
||||
void loop() override;
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
// read a given register
|
||||
bool read_reg_(uint8_t reg, uint8_t *value);
|
||||
// write a value to a given register
|
||||
@@ -46,6 +50,8 @@ class MCP23016 : public Component, public i2c::I2CDevice {
|
||||
|
||||
uint8_t olat_0_{0x00};
|
||||
uint8_t olat_1_{0x00};
|
||||
// Cache for input values (16-bit combined for both banks)
|
||||
uint16_t input_mask_{0x00};
|
||||
};
|
||||
|
||||
class MCP23016GPIOPin : public GPIOPin {
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
|
||||
CODEOWNERS = ["@Mat931"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
MULTI_CONF = True
|
||||
pca6416a_ns = cg.esphome_ns.namespace("pca6416a")
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@ void PCA6416AComponent::setup() {
|
||||
this->status_has_error());
|
||||
}
|
||||
|
||||
void PCA6416AComponent::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
}
|
||||
|
||||
void PCA6416AComponent::dump_config() {
|
||||
if (this->has_pullup_) {
|
||||
ESP_LOGCONFIG(TAG, "PCAL6416A:");
|
||||
@@ -63,15 +68,25 @@ void PCA6416AComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
bool PCA6416AComponent::digital_read(uint8_t pin) {
|
||||
uint8_t bit = pin % 8;
|
||||
bool PCA6416AComponent::digital_read_hw(uint8_t pin) {
|
||||
uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1;
|
||||
uint8_t value = 0;
|
||||
this->read_register_(reg_addr, &value);
|
||||
return value & (1 << bit);
|
||||
if (!this->read_register_(reg_addr, &value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the appropriate part of input_mask_
|
||||
if (pin < 8) {
|
||||
this->input_mask_ = (this->input_mask_ & 0xFF00) | value;
|
||||
} else {
|
||||
this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PCA6416AComponent::digital_write(uint8_t pin, bool value) {
|
||||
bool PCA6416AComponent::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
|
||||
void PCA6416AComponent::digital_write_hw(uint8_t pin, bool value) {
|
||||
uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1;
|
||||
this->update_register_(pin, value, reg_addr);
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pca6416a {
|
||||
|
||||
class PCA6416AComponent : public Component, public i2c::I2CDevice {
|
||||
class PCA6416AComponent : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
|
||||
public:
|
||||
PCA6416AComponent() = default;
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Helper function to read the value of a pin.
|
||||
bool digital_read(uint8_t pin);
|
||||
/// Helper function to write the value of a pin.
|
||||
void digital_write(uint8_t pin, bool value);
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
@@ -25,6 +25,11 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice {
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
bool read_register_(uint8_t reg, uint8_t *value);
|
||||
bool write_register_(uint8_t reg, uint8_t value);
|
||||
void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr);
|
||||
@@ -32,6 +37,8 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice {
|
||||
/// The mask to write as output state - 1 means HIGH, 0 means LOW
|
||||
uint8_t output_0_{0x00};
|
||||
uint8_t output_1_{0x00};
|
||||
/// Cache for input values (16-bit combined for both banks)
|
||||
uint16_t input_mask_{0x00};
|
||||
/// Storage for last I2C error seen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
/// Only the PCAL6416A has pull-up resistors
|
||||
|
||||
@@ -11,7 +11,8 @@ from esphome.const import (
|
||||
CONF_OUTPUT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@hwstar", "@clydebarrow"]
|
||||
CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"]
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
CONF_PIN_COUNT = "pin_count"
|
||||
|
||||
@@ -37,10 +37,9 @@ void PCA9554Component::setup() {
|
||||
}
|
||||
|
||||
void PCA9554Component::loop() {
|
||||
// The read_inputs_() method will cache the input values from the chip.
|
||||
this->read_inputs_();
|
||||
// Clear all the previously read flags.
|
||||
this->was_previously_read_ = 0x00;
|
||||
// Invalidate the cache at the start of each loop.
|
||||
// The actual read will happen on demand when digital_read() is called
|
||||
this->reset_pin_cache_();
|
||||
}
|
||||
|
||||
void PCA9554Component::dump_config() {
|
||||
@@ -54,21 +53,17 @@ void PCA9554Component::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
bool PCA9554Component::digital_read(uint8_t pin) {
|
||||
// Note: We want to try and avoid doing any I2C bus read transactions here
|
||||
// to conserve I2C bus bandwidth. So what we do is check to see if we
|
||||
// have seen a read during the time esphome is running this loop. If we have,
|
||||
// we do an I2C bus transaction to get the latest value. If we haven't
|
||||
// we return a cached value which was read at the time loop() was called.
|
||||
if (this->was_previously_read_ & (1 << pin))
|
||||
this->read_inputs_(); // Force a read of a new value
|
||||
// Indicate we saw a read request for this pin in case a
|
||||
// read happens later in the same loop.
|
||||
this->was_previously_read_ |= (1 << pin);
|
||||
bool PCA9554Component::digital_read_hw(uint8_t pin) {
|
||||
// Read all pins from hardware into input_mask_
|
||||
return this->read_inputs_(); // Return true if I2C read succeeded, false on error
|
||||
}
|
||||
|
||||
bool PCA9554Component::digital_read_cache(uint8_t pin) {
|
||||
// Return the cached pin state from input_mask_
|
||||
return this->input_mask_ & (1 << pin);
|
||||
}
|
||||
|
||||
void PCA9554Component::digital_write(uint8_t pin, bool value) {
|
||||
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
@@ -127,8 +122,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) {
|
||||
|
||||
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
// Run our loop() method very early in the loop, so that we cache read values before
|
||||
// before other components call our digital_read() method.
|
||||
// Run our loop() method early to invalidate cache before any other components access the pins
|
||||
float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
|
||||
|
||||
void PCA9554GPIOPin::setup() { pin_mode(flags_); }
|
||||
|
||||
@@ -3,22 +3,21 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pca9554 {
|
||||
|
||||
class PCA9554Component : public Component, public i2c::I2CDevice {
|
||||
class PCA9554Component : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
PCA9554Component() = default;
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Poll for input changes periodically
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to read the value of a pin.
|
||||
bool digital_read(uint8_t pin);
|
||||
/// Helper function to write the value of a pin.
|
||||
void digital_write(uint8_t pin, bool value);
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
@@ -32,9 +31,13 @@ class PCA9554Component : public Component, public i2c::I2CDevice {
|
||||
|
||||
protected:
|
||||
bool read_inputs_();
|
||||
|
||||
bool write_register_(uint8_t reg, uint16_t value);
|
||||
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
/// number of bits the expander has
|
||||
size_t pin_count_{8};
|
||||
/// width of registers
|
||||
@@ -45,8 +48,6 @@ class PCA9554Component : public Component, public i2c::I2CDevice {
|
||||
uint16_t output_mask_{0x00};
|
||||
/// The state of the actual input pin states - 1 means HIGH, 0 means LOW
|
||||
uint16_t input_mask_{0x00};
|
||||
/// Flags to check if read previously during this loop
|
||||
uint16_t was_previously_read_ = {0x00};
|
||||
/// Storage for last I2C error seen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_OUTPUT,
|
||||
)
|
||||
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ void PCF8574Component::setup() {
|
||||
this->write_gpio_();
|
||||
this->read_gpio_();
|
||||
}
|
||||
void PCF8574Component::loop() {
|
||||
// Invalidate the cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
}
|
||||
void PCF8574Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PCF8574:");
|
||||
LOG_I2C_DEVICE(this)
|
||||
@@ -24,17 +28,19 @@ void PCF8574Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
bool PCF8574Component::digital_read(uint8_t pin) {
|
||||
this->read_gpio_();
|
||||
return this->input_mask_ & (1 << pin);
|
||||
bool PCF8574Component::digital_read_hw(uint8_t pin) {
|
||||
// Read all pins from hardware into input_mask_
|
||||
return this->read_gpio_(); // Return true if I2C read succeeded, false on error
|
||||
}
|
||||
void PCF8574Component::digital_write(uint8_t pin, bool value) {
|
||||
|
||||
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
|
||||
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
|
||||
this->write_gpio_();
|
||||
}
|
||||
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
@@ -91,6 +97,9 @@ bool PCF8574Component::write_gpio_() {
|
||||
}
|
||||
float PCF8574Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
// Run our loop() method early to invalidate cache before any other components access the pins
|
||||
float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI
|
||||
|
||||
void PCF8574GPIOPin::setup() { pin_mode(flags_); }
|
||||
void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace pcf8574 {
|
||||
|
||||
class PCF8574Component : public Component, public i2c::I2CDevice {
|
||||
// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction
|
||||
// so we use uint16_t as bank type to ensure all pins are in one bank and cached together
|
||||
class PCF8574Component : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
PCF8574Component() = default;
|
||||
|
||||
@@ -15,20 +20,22 @@ class PCF8574Component : public Component, public i2c::I2CDevice {
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
/// Helper function to read the value of a pin.
|
||||
bool digital_read(uint8_t pin);
|
||||
/// Helper function to write the value of a pin.
|
||||
void digital_write(uint8_t pin, bool value);
|
||||
/// Invalidate cache at start of each loop
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
float get_loop_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
bool read_gpio_();
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
bool read_gpio_();
|
||||
bool write_gpio_();
|
||||
|
||||
/// Mask for the pin mode - 1 means output, 0 means input
|
||||
|
||||
@@ -18,7 +18,7 @@ void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) {
|
||||
|
||||
void PulseWidthSensor::dump_config() {
|
||||
LOG_SENSOR("", "Pulse Width", this);
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
}
|
||||
void PulseWidthSensor::update() {
|
||||
|
||||
@@ -25,7 +25,7 @@ CONF_SCAN_TIME = "scan_time"
|
||||
CONF_DEBOUNCE_TIME = "debounce_time"
|
||||
CONF_SX1509_ID = "sx1509_id"
|
||||
|
||||
AUTO_LOAD = ["key_provider"]
|
||||
AUTO_LOAD = ["key_provider", "gpio_expander"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ void SX1509Component::dump_config() {
|
||||
}
|
||||
|
||||
void SX1509Component::loop() {
|
||||
// Reset cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
|
||||
if (this->has_keypad_) {
|
||||
if (millis() - this->last_loop_timestamp_ < min_loop_period_)
|
||||
return;
|
||||
@@ -73,18 +76,20 @@ void SX1509Component::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool SX1509Component::digital_read(uint8_t pin) {
|
||||
bool SX1509Component::digital_read_hw(uint8_t pin) {
|
||||
// Always read all pins when any input pin is accessed
|
||||
return this->read_byte_16(REG_DATA_B, &this->input_mask_);
|
||||
}
|
||||
|
||||
bool SX1509Component::digital_read_cache(uint8_t pin) {
|
||||
// Return cached value for input pins, false for output pins
|
||||
if (this->ddr_mask_ & (1 << pin)) {
|
||||
uint16_t temp_reg_data;
|
||||
if (!this->read_byte_16(REG_DATA_B, &temp_reg_data))
|
||||
return false;
|
||||
if (temp_reg_data & (1 << pin))
|
||||
return true;
|
||||
return (this->input_mask_ & (1 << pin)) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SX1509Component::digital_write(uint8_t pin, bool bit_value) {
|
||||
void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
|
||||
if ((~this->ddr_mask_) & (1 << pin)) {
|
||||
// If the pin is an output, write high/low
|
||||
uint16_t temp_reg_data = 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/key_provider/key_provider.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "sx1509_gpio_pin.h"
|
||||
@@ -30,7 +31,10 @@ class SX1509Processor {
|
||||
|
||||
class SX1509KeyTrigger : public Trigger<uint8_t> {};
|
||||
|
||||
class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider {
|
||||
class SX1509Component : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16>,
|
||||
public key_provider::KeyProvider {
|
||||
public:
|
||||
SX1509Component() = default;
|
||||
|
||||
@@ -39,11 +43,9 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void loop() override;
|
||||
|
||||
bool digital_read(uint8_t pin);
|
||||
uint16_t read_key_data();
|
||||
void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); };
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
void digital_write(uint8_t pin, bool bit_value);
|
||||
uint32_t get_clock() { return this->clk_x_; };
|
||||
void set_rows_cols(uint8_t rows, uint8_t cols) {
|
||||
this->rows_ = rows;
|
||||
@@ -61,10 +63,15 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov
|
||||
void setup_led_driver(uint8_t pin);
|
||||
|
||||
protected:
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
uint32_t clk_x_ = 2000000;
|
||||
uint8_t frequency_ = 0;
|
||||
uint16_t ddr_mask_ = 0x00;
|
||||
uint16_t input_mask_ = 0x00;
|
||||
uint16_t input_mask_ = 0x00; // Cache for input values (16-bit for all pins)
|
||||
uint16_t port_mask_ = 0x00;
|
||||
uint16_t output_state_ = 0x00;
|
||||
bool has_keypad_ = false;
|
||||
|
||||
@@ -104,7 +104,7 @@ void UFireECComponent::write_data_(uint8_t reg, float data) {
|
||||
void UFireECComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "uFire-EC");
|
||||
LOG_I2C_DEVICE(this)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_);
|
||||
|
||||
@@ -141,7 +141,7 @@ void UFireISEComponent::write_data_(uint8_t reg, float data) {
|
||||
void UFireISEComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "uFire-ISE");
|
||||
LOG_I2C_DEVICE(this)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_);
|
||||
|
||||
@@ -181,7 +181,7 @@ void WaveshareEPaper2P13InV3::dump_config() {
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_)
|
||||
LOG_PIN(" DC Pin: ", this->dc_pin_)
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) {
|
||||
|
||||
@@ -340,6 +340,18 @@ void Component::status_momentary_error(const std::string &name, uint32_t length)
|
||||
this->set_timeout(name, length, [this]() { this->status_clear_error(); });
|
||||
}
|
||||
void Component::dump_config() {}
|
||||
|
||||
// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size
|
||||
void log_update_interval(const char *tag, PollingComponent *component) {
|
||||
uint32_t update_interval = component->get_update_interval();
|
||||
if (update_interval == SCHEDULER_DONT_RUN) {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: never");
|
||||
} else if (update_interval < 100) {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f);
|
||||
} else {
|
||||
ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f);
|
||||
}
|
||||
}
|
||||
float Component::get_actual_setup_priority() const {
|
||||
// Check if there's an override in the global vector
|
||||
if (setup_priority_overrides) {
|
||||
|
||||
@@ -48,14 +48,13 @@ extern const float LATE;
|
||||
|
||||
static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
|
||||
|
||||
#define LOG_UPDATE_INTERVAL(this) \
|
||||
if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \
|
||||
ESP_LOGCONFIG(TAG, " Update Interval: never"); \
|
||||
} else if (this->get_update_interval() < 100) { \
|
||||
ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \
|
||||
} else { \
|
||||
ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
|
||||
}
|
||||
// Forward declaration
|
||||
class PollingComponent;
|
||||
|
||||
// Function declaration for LOG_UPDATE_INTERVAL
|
||||
void log_update_interval(const char *tag, PollingComponent *component);
|
||||
|
||||
#define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this)
|
||||
|
||||
extern const uint8_t COMPONENT_STATE_MASK;
|
||||
extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
|
||||
|
||||
@@ -14,7 +14,19 @@ namespace esphome {
|
||||
|
||||
static const char *const TAG = "scheduler";
|
||||
|
||||
static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10;
|
||||
// Memory pool configuration constants
|
||||
// Pool size of 5 matches typical usage patterns (2-4 active timers)
|
||||
// - Minimal memory overhead (~250 bytes on ESP32)
|
||||
// - Sufficient for most configs with a couple sensors/components
|
||||
// - Still prevents heap fragmentation and allocation stalls
|
||||
// - Complex setups with many timers will just allocate beyond the pool
|
||||
// See https://github.com/esphome/backlog/issues/52
|
||||
static constexpr size_t MAX_POOL_SIZE = 5;
|
||||
|
||||
// Maximum number of logically deleted (cancelled) items before forcing cleanup.
|
||||
// Set to 5 to match the pool size - when we have as many cancelled items as our
|
||||
// pool can hold, it's time to clean up and recycle them.
|
||||
static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5;
|
||||
// Half the 32-bit range - used to detect rollovers vs normal time progression
|
||||
static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits<uint32_t>::max() / 2;
|
||||
// max delay to start an interval sequence
|
||||
@@ -79,8 +91,28 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself
|
||||
const uint64_t now = this->millis_64_(millis());
|
||||
|
||||
// Take lock early to protect scheduler_item_pool_ access
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// Create and populate the scheduler item
|
||||
auto item = make_unique<SchedulerItem>();
|
||||
std::unique_ptr<SchedulerItem> item;
|
||||
if (!this->scheduler_item_pool_.empty()) {
|
||||
// Reuse from pool
|
||||
item = std::move(this->scheduler_item_pool_.back());
|
||||
this->scheduler_item_pool_.pop_back();
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
} else {
|
||||
// Allocate new if pool is empty
|
||||
item = make_unique<SchedulerItem>();
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Allocated new item (pool empty)");
|
||||
#endif
|
||||
}
|
||||
item->component = component;
|
||||
item->set_name(name_cstr, !is_static_string);
|
||||
item->type = type;
|
||||
@@ -99,7 +131,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
// Single-core platforms don't need thread-safe defer handling
|
||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||
// Put in defer queue for guaranteed FIFO execution
|
||||
LockGuard guard{this->lock_};
|
||||
if (!skip_cancel) {
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
}
|
||||
@@ -108,21 +139,18 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
|
||||
const auto now = this->millis_64_(millis()); // Fresh millis() call
|
||||
|
||||
// Type-specific setup
|
||||
if (type == SchedulerItem::INTERVAL) {
|
||||
item->interval = delay;
|
||||
// first execution happens immediately after a random smallish offset
|
||||
// Calculate random offset (0 to min(interval/2, 5s))
|
||||
uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float());
|
||||
item->next_execution_ = now + offset;
|
||||
item->set_next_execution(now + offset);
|
||||
ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay,
|
||||
offset);
|
||||
} else {
|
||||
item->interval = 0;
|
||||
item->next_execution_ = now + delay;
|
||||
item->set_next_execution(now + delay);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
@@ -138,12 +166,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
name_cstr ? name_cstr : "(null)", type_str, delay);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()),
|
||||
name_cstr ? name_cstr : "(null)", type_str, delay, static_cast<uint32_t>(item->next_execution_ - now));
|
||||
name_cstr ? name_cstr : "(null)", type_str, delay,
|
||||
static_cast<uint32_t>(item->get_next_execution() - now));
|
||||
}
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
LockGuard guard{this->lock_};
|
||||
|
||||
// For retries, check if there's a cancelled timeout first
|
||||
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
|
||||
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) ||
|
||||
@@ -285,9 +312,10 @@ optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
|
||||
auto &item = this->items_[0];
|
||||
// Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit
|
||||
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller
|
||||
if (item->next_execution_ < now_64)
|
||||
const uint64_t next_exec = item->get_next_execution();
|
||||
if (next_exec < now_64)
|
||||
return 0;
|
||||
return item->next_execution_ - now_64;
|
||||
return next_exec - now_64;
|
||||
}
|
||||
void HOT Scheduler::call(uint32_t now) {
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
@@ -319,6 +347,8 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
if (!this->should_skip_item_(item.get())) {
|
||||
this->execute_item_(item.get(), now);
|
||||
}
|
||||
// Recycle the defer item after execution
|
||||
this->recycle_item_(std::move(item));
|
||||
}
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
@@ -326,6 +356,9 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop()
|
||||
this->process_to_add();
|
||||
|
||||
// Track if any items were added to to_add_ during this call (intervals or from callbacks)
|
||||
bool has_added_items = false;
|
||||
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
static uint64_t last_print = 0;
|
||||
|
||||
@@ -335,11 +368,11 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
|
||||
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
|
||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||
major_dbg, last_dbg);
|
||||
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
|
||||
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
|
||||
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
|
||||
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
|
||||
this->millis_major_, this->last_millis_);
|
||||
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
|
||||
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
|
||||
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
|
||||
// Cleanup before debug output
|
||||
this->cleanup_();
|
||||
@@ -352,9 +385,10 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
|
||||
const char *name = item->get_name();
|
||||
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64,
|
||||
bool is_cancelled = is_item_removed_(item.get());
|
||||
ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s",
|
||||
item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval,
|
||||
item->next_execution_ - now_64, item->next_execution_);
|
||||
item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : "");
|
||||
|
||||
old_items.push_back(std::move(item));
|
||||
}
|
||||
@@ -369,8 +403,13 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
// If we have too many items to remove
|
||||
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
|
||||
// Cleanup removed items before processing
|
||||
// First try to clean items from the top of the heap (fast path)
|
||||
this->cleanup_();
|
||||
|
||||
// If we still have too many cancelled items, do a full cleanup
|
||||
// This only happens if cancelled items are stuck in the middle/bottom of the heap
|
||||
if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) {
|
||||
// We hold the lock for the entire cleanup operation because:
|
||||
// 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
|
||||
// 2. Other threads must see either the old state or the new state, not intermediate states
|
||||
@@ -380,10 +419,13 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
|
||||
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
|
||||
|
||||
// Move all non-removed items to valid_items
|
||||
// Move all non-removed items to valid_items, recycle removed ones
|
||||
for (auto &item : this->items_) {
|
||||
if (!item->remove) {
|
||||
if (!is_item_removed_(item.get())) {
|
||||
valid_items.push_back(std::move(item));
|
||||
} else {
|
||||
// Recycle removed items
|
||||
this->recycle_item_(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,15 +435,12 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
this->to_remove_ = 0;
|
||||
}
|
||||
|
||||
// Cleanup removed items before processing
|
||||
this->cleanup_();
|
||||
while (!this->items_.empty()) {
|
||||
// use scoping to indicate visibility of `item` variable
|
||||
{
|
||||
// Don't copy-by value yet
|
||||
auto &item = this->items_[0];
|
||||
if (item->next_execution_ > now_64) {
|
||||
if (item->get_next_execution() > now_64) {
|
||||
// Not reached timeout yet, done for this call
|
||||
break;
|
||||
}
|
||||
@@ -440,7 +479,7 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
const char *item_name = item->get_name();
|
||||
ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")",
|
||||
item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval,
|
||||
item->next_execution_, now_64);
|
||||
item->get_next_execution(), now_64);
|
||||
#endif /* ESPHOME_DEBUG_SCHEDULER */
|
||||
|
||||
// Warning: During callback(), a lot of stuff can happen, including:
|
||||
@@ -465,20 +504,29 @@ void HOT Scheduler::call(uint32_t now) {
|
||||
}
|
||||
|
||||
if (item->type == SchedulerItem::INTERVAL) {
|
||||
item->next_execution_ = now_64 + item->interval;
|
||||
item->set_next_execution(now_64 + item->interval);
|
||||
// Add new item directly to to_add_
|
||||
// since we have the lock held
|
||||
this->to_add_.push_back(std::move(item));
|
||||
} else {
|
||||
// Timeout completed - recycle it
|
||||
this->recycle_item_(std::move(item));
|
||||
}
|
||||
|
||||
has_added_items |= !this->to_add_.empty();
|
||||
}
|
||||
}
|
||||
|
||||
this->process_to_add();
|
||||
if (has_added_items) {
|
||||
this->process_to_add();
|
||||
}
|
||||
}
|
||||
void HOT Scheduler::process_to_add() {
|
||||
LockGuard guard{this->lock_};
|
||||
for (auto &it : this->to_add_) {
|
||||
if (it->remove) {
|
||||
if (is_item_removed_(it.get())) {
|
||||
// Recycle cancelled items
|
||||
this->recycle_item_(std::move(it));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -518,6 +566,10 @@ size_t HOT Scheduler::cleanup_() {
|
||||
}
|
||||
void HOT Scheduler::pop_raw_() {
|
||||
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
|
||||
|
||||
// Instead of destroying, recycle the item
|
||||
this->recycle_item_(std::move(this->items_.back()));
|
||||
|
||||
this->items_.pop_back();
|
||||
}
|
||||
|
||||
@@ -552,7 +604,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
|
||||
// Check all containers for matching items
|
||||
#ifndef ESPHOME_THREAD_SINGLE
|
||||
// Only check defer queue for timeouts (intervals never go there)
|
||||
// Mark items in defer queue as cancelled (they'll be skipped when processed)
|
||||
if (type == SchedulerItem::TIMEOUT) {
|
||||
for (auto &item : this->defer_queue_) {
|
||||
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
|
||||
@@ -564,11 +616,22 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
#endif /* not ESPHOME_THREAD_SINGLE */
|
||||
|
||||
// Cancel items in the main heap
|
||||
for (auto &item : this->items_) {
|
||||
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
|
||||
this->mark_item_removed_(item.get());
|
||||
// Special case: if the last item in the heap matches, we can remove it immediately
|
||||
// (removing the last element doesn't break heap structure)
|
||||
if (!this->items_.empty()) {
|
||||
auto &last_item = this->items_.back();
|
||||
if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) {
|
||||
this->recycle_item_(std::move(this->items_.back()));
|
||||
this->items_.pop_back();
|
||||
total_cancelled++;
|
||||
this->to_remove_++; // Track removals for heap items
|
||||
}
|
||||
// For other items in heap, we can only mark for removal (can't remove from middle of heap)
|
||||
for (auto &item : this->items_) {
|
||||
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
|
||||
this->mark_item_removed_(item.get());
|
||||
total_cancelled++;
|
||||
this->to_remove_++; // Track removals for heap items
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,7 +807,31 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
|
||||
|
||||
bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
|
||||
const std::unique_ptr<SchedulerItem> &b) {
|
||||
return a->next_execution_ > b->next_execution_;
|
||||
// High bits are almost always equal (change only on 32-bit rollover ~49 days)
|
||||
// Optimize for common case: check low bits first when high bits are equal
|
||||
return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_)
|
||||
: (a->next_execution_high_ > b->next_execution_high_);
|
||||
}
|
||||
|
||||
void Scheduler::recycle_item_(std::unique_ptr<SchedulerItem> item) {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) {
|
||||
// Clear callback to release captured resources
|
||||
item->callback = nullptr;
|
||||
// Clear dynamic name if any
|
||||
item->clear_dynamic_name();
|
||||
this->scheduler_item_pool_.push_back(std::move(item));
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
} else {
|
||||
#ifdef ESPHOME_DEBUG_SCHEDULER
|
||||
ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size());
|
||||
#endif
|
||||
}
|
||||
// else: unique_ptr will delete the item when it goes out of scope
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -88,19 +88,22 @@ class Scheduler {
|
||||
struct SchedulerItem {
|
||||
// Ordered by size to minimize padding
|
||||
Component *component;
|
||||
uint32_t interval;
|
||||
// 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis()
|
||||
// with a 16-bit rollover counter to create a 64-bit time that won't roll over for
|
||||
// billions of years. This ensures correct scheduling even when devices run for months.
|
||||
uint64_t next_execution_;
|
||||
|
||||
// Optimized name storage using tagged union
|
||||
union {
|
||||
const char *static_name; // For string literals (no allocation)
|
||||
char *dynamic_name; // For allocated strings
|
||||
} 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 (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)
|
||||
std::function<void()> callback;
|
||||
uint16_t next_execution_high_; // Upper 16 bits (millis_major counter)
|
||||
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// Multi-threaded with atomics: use atomic for lock-free access
|
||||
@@ -126,7 +129,8 @@ class Scheduler {
|
||||
SchedulerItem()
|
||||
: component(nullptr),
|
||||
interval(0),
|
||||
next_execution_(0),
|
||||
next_execution_low_(0),
|
||||
next_execution_high_(0),
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
// remove is initialized in the member declaration as std::atomic<bool>{false}
|
||||
type(TIMEOUT),
|
||||
@@ -142,11 +146,7 @@ class Scheduler {
|
||||
}
|
||||
|
||||
// Destructor to clean up dynamic names
|
||||
~SchedulerItem() {
|
||||
if (name_is_dynamic) {
|
||||
delete[] name_.dynamic_name;
|
||||
}
|
||||
}
|
||||
~SchedulerItem() { clear_dynamic_name(); }
|
||||
|
||||
// Delete copy operations to prevent accidental copies
|
||||
SchedulerItem(const SchedulerItem &) = delete;
|
||||
@@ -159,13 +159,19 @@ class Scheduler {
|
||||
// Helper to get the name regardless of storage type
|
||||
const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
|
||||
|
||||
// Helper to clear dynamic name if allocated
|
||||
void clear_dynamic_name() {
|
||||
if (name_is_dynamic && name_.dynamic_name) {
|
||||
delete[] name_.dynamic_name;
|
||||
name_.dynamic_name = nullptr;
|
||||
name_is_dynamic = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to set name with proper ownership
|
||||
void set_name(const char *name, bool make_copy = false) {
|
||||
// Clean up old dynamic name if any
|
||||
if (name_is_dynamic && name_.dynamic_name) {
|
||||
delete[] name_.dynamic_name;
|
||||
name_is_dynamic = false;
|
||||
}
|
||||
clear_dynamic_name();
|
||||
|
||||
if (!name) {
|
||||
// nullptr case - no name provided
|
||||
@@ -183,7 +189,21 @@ class Scheduler {
|
||||
}
|
||||
|
||||
static bool cmp(const std::unique_ptr<SchedulerItem> &a, const std::unique_ptr<SchedulerItem> &b);
|
||||
const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
|
||||
|
||||
// Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility.
|
||||
// The upper 16 bits of the 64-bit value are always zero, which is fine since
|
||||
// millis_major_ is also 16 bits and they must match.
|
||||
constexpr uint64_t get_next_execution() const {
|
||||
return (static_cast<uint64_t>(next_execution_high_) << 32) | next_execution_low_;
|
||||
}
|
||||
|
||||
constexpr void set_next_execution(uint64_t value) {
|
||||
next_execution_low_ = static_cast<uint32_t>(value);
|
||||
// Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits.
|
||||
// This is correct because millis_major_ that creates these values is also 16 bits.
|
||||
next_execution_high_ = static_cast<uint16_t>(value >> 32);
|
||||
}
|
||||
constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; }
|
||||
const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); }
|
||||
};
|
||||
|
||||
@@ -214,6 +234,15 @@ class Scheduler {
|
||||
// Common implementation for cancel operations
|
||||
bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type);
|
||||
|
||||
// Helper to check if two scheduler item names match
|
||||
inline bool HOT names_match_(const char *name1, const char *name2) const {
|
||||
// Check pointer equality first (common for static strings), then string contents
|
||||
// The core ESPHome codebase uses static strings (const char*) for component names,
|
||||
// making pointer comparison effective. The std::string overloads exist only for
|
||||
// compatibility with external components but are rarely used in practice.
|
||||
return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0));
|
||||
}
|
||||
|
||||
// Helper function to check if item matches criteria for cancellation
|
||||
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
|
||||
SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
|
||||
@@ -221,29 +250,20 @@ class Scheduler {
|
||||
(match_retry && !item->is_retry)) {
|
||||
return false;
|
||||
}
|
||||
const char *item_name = item->get_name();
|
||||
if (item_name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Fast path: if pointers are equal
|
||||
// This is effective because the core ESPHome codebase uses static strings (const char*)
|
||||
// for component names. The std::string overloads exist only for compatibility with
|
||||
// external components, but are rarely used in practice.
|
||||
if (item_name == name_cstr) {
|
||||
return true;
|
||||
}
|
||||
// Slow path: compare string contents
|
||||
return strcmp(name_cstr, item_name) == 0;
|
||||
return this->names_match_(item->get_name(), name_cstr);
|
||||
}
|
||||
|
||||
// Helper to execute a scheduler item
|
||||
void execute_item_(SchedulerItem *item, uint32_t now);
|
||||
|
||||
// Helper to check if item should be skipped
|
||||
bool should_skip_item_(const SchedulerItem *item) const {
|
||||
return item->remove || (item->component != nullptr && item->component->is_failed());
|
||||
bool should_skip_item_(SchedulerItem *item) const {
|
||||
return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed());
|
||||
}
|
||||
|
||||
// Helper to recycle a SchedulerItem
|
||||
void recycle_item_(std::unique_ptr<SchedulerItem> item);
|
||||
|
||||
// Helper to check if item is marked for removal (platform-specific)
|
||||
// Returns true if item should be skipped, handles platform-specific synchronization
|
||||
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
|
||||
@@ -280,8 +300,9 @@ class Scheduler {
|
||||
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
|
||||
bool match_retry) const {
|
||||
for (const auto &item : container) {
|
||||
if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
if (is_item_removed_(item.get()) &&
|
||||
this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
|
||||
/* skip_removed= */ false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -297,6 +318,16 @@ class Scheduler {
|
||||
#endif /* ESPHOME_THREAD_SINGLE */
|
||||
uint32_t to_remove_{0};
|
||||
|
||||
// Memory pool for recycling SchedulerItem objects to reduce heap churn.
|
||||
// Design decisions:
|
||||
// - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items
|
||||
// - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups
|
||||
// - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32)
|
||||
// - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation
|
||||
// can stall the entire system, causing timing issues and dropped events for any components that need
|
||||
// to synchronize between tasks (see https://github.com/esphome/backlog/issues/52)
|
||||
std::vector<std::unique_ptr<SchedulerItem>> scheduler_item_pool_;
|
||||
|
||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
|
||||
/*
|
||||
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates
|
||||
|
||||
@@ -308,8 +308,12 @@ def perform_ota(
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def run_ota_impl_(remote_host, remote_port, password, filename):
|
||||
def run_ota_impl_(
|
||||
remote_host: str | list[str], remote_port: int, password: str, filename: str
|
||||
) -> int:
|
||||
# Handle both single host and list of hosts
|
||||
try:
|
||||
# Resolve all hosts at once for parallel DNS resolution
|
||||
res = resolve_ip_address(remote_host, remote_port)
|
||||
except EsphomeError as err:
|
||||
_LOGGER.error(
|
||||
@@ -350,7 +354,9 @@ def run_ota_impl_(remote_host, remote_port, password, filename):
|
||||
return 1
|
||||
|
||||
|
||||
def run_ota(remote_host, remote_port, password, filename):
|
||||
def run_ota(
|
||||
remote_host: str | list[str], remote_port: int, password: str, filename: str
|
||||
) -> int:
|
||||
try:
|
||||
return run_ota_impl_(remote_host, remote_port, password, filename)
|
||||
except OTAError as err:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
from contextlib import suppress
|
||||
import ipaddress
|
||||
@@ -11,6 +13,18 @@ from urllib.parse import urlparse
|
||||
|
||||
from esphome.const import __version__ as ESPHOME_VERSION
|
||||
|
||||
# Type aliases for socket address information
|
||||
AddrInfo = tuple[
|
||||
int, # family (AF_INET, AF_INET6, etc.)
|
||||
int, # type (SOCK_STREAM, SOCK_DGRAM, etc.)
|
||||
int, # proto (IPPROTO_TCP, etc.)
|
||||
str, # canonname
|
||||
tuple[str, int] | tuple[str, int, int, int], # sockaddr (IPv4 or IPv6)
|
||||
]
|
||||
IPv4SockAddr = tuple[str, int] # (host, port)
|
||||
IPv6SockAddr = tuple[str, int, int, int] # (host, port, flowinfo, scope_id)
|
||||
SockAddr = IPv4SockAddr | IPv6SockAddr
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
IS_MACOS = platform.system() == "Darwin"
|
||||
@@ -147,32 +161,7 @@ def is_ip_address(host):
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_with_zeroconf(host):
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.zeroconf import EsphomeZeroconf
|
||||
|
||||
try:
|
||||
zc = EsphomeZeroconf()
|
||||
except Exception as err:
|
||||
raise EsphomeError(
|
||||
"Cannot start mDNS sockets, is this a docker container without "
|
||||
"host network mode?"
|
||||
) from err
|
||||
try:
|
||||
info = zc.resolve_host(f"{host}.")
|
||||
except Exception as err:
|
||||
raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err
|
||||
finally:
|
||||
zc.close()
|
||||
if info is None:
|
||||
raise EsphomeError(
|
||||
"Error resolving address with mDNS: Did not respond. "
|
||||
"Maybe the device is offline."
|
||||
)
|
||||
return info
|
||||
|
||||
|
||||
def addr_preference_(res):
|
||||
def addr_preference_(res: AddrInfo) -> int:
|
||||
# Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then
|
||||
# Legacy IP, then IPv6 link-local addresses without an actual link.
|
||||
sa = res[4]
|
||||
@@ -184,66 +173,70 @@ def addr_preference_(res):
|
||||
return 1
|
||||
|
||||
|
||||
def resolve_ip_address(host, port):
|
||||
def resolve_ip_address(host: str | list[str], port: int) -> list[AddrInfo]:
|
||||
import socket
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
# There are five cases here. The host argument could be one of:
|
||||
# • a *list* of IP addresses discovered by MQTT,
|
||||
# • a single IP address specified by the user,
|
||||
# • a .local hostname to be resolved by mDNS,
|
||||
# • a normal hostname to be resolved in DNS, or
|
||||
# • A URL from which we should extract the hostname.
|
||||
#
|
||||
# In each of the first three cases, we end up with IP addresses in
|
||||
# string form which need to be converted to a 5-tuple to be used
|
||||
# for the socket connection attempt. The easiest way to construct
|
||||
# those is to pass the IP address string to getaddrinfo(). Which,
|
||||
# coincidentally, is how we do hostname lookups in the other cases
|
||||
# too. So first build a list which contains either IP addresses or
|
||||
# a single hostname, then call getaddrinfo() on each element of
|
||||
# that list.
|
||||
|
||||
errs = []
|
||||
hosts: list[str]
|
||||
if isinstance(host, list):
|
||||
addr_list = host
|
||||
elif is_ip_address(host):
|
||||
addr_list = [host]
|
||||
hosts = host
|
||||
else:
|
||||
url = urlparse(host)
|
||||
if url.scheme != "":
|
||||
host = url.hostname
|
||||
if not is_ip_address(host):
|
||||
url = urlparse(host)
|
||||
if url.scheme != "":
|
||||
host = url.hostname
|
||||
hosts = [host]
|
||||
|
||||
addr_list = []
|
||||
if host.endswith(".local"):
|
||||
res: list[AddrInfo] = []
|
||||
if all(is_ip_address(h) for h in hosts):
|
||||
# Fast path: all are IP addresses, use socket.getaddrinfo with AI_NUMERICHOST
|
||||
for addr in hosts:
|
||||
try:
|
||||
_LOGGER.info("Resolving IP address of %s in mDNS", host)
|
||||
addr_list = _resolve_with_zeroconf(host)
|
||||
except EsphomeError as err:
|
||||
errs.append(str(err))
|
||||
res += socket.getaddrinfo(
|
||||
addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST
|
||||
)
|
||||
except OSError:
|
||||
_LOGGER.debug("Failed to parse IP address '%s'", addr)
|
||||
# Sort by preference
|
||||
res.sort(key=addr_preference_)
|
||||
return res
|
||||
|
||||
# If not mDNS, or if mDNS failed, use normal DNS
|
||||
if not addr_list:
|
||||
addr_list = [host]
|
||||
from esphome.resolver import AsyncResolver
|
||||
|
||||
# Now we have a list containing either IP addresses or a hostname
|
||||
res = []
|
||||
for addr in addr_list:
|
||||
if not is_ip_address(addr):
|
||||
_LOGGER.info("Resolving IP address of %s", host)
|
||||
try:
|
||||
r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP)
|
||||
except OSError as err:
|
||||
errs.append(str(err))
|
||||
raise EsphomeError(
|
||||
f"Error resolving IP address: {', '.join(errs)}"
|
||||
) from err
|
||||
resolver = AsyncResolver(hosts, port)
|
||||
addr_infos = resolver.resolve()
|
||||
# Convert aioesphomeapi AddrInfo to our format
|
||||
for addr_info in addr_infos:
|
||||
sockaddr = addr_info.sockaddr
|
||||
if addr_info.family == socket.AF_INET6:
|
||||
# IPv6
|
||||
sockaddr_tuple = (
|
||||
sockaddr.address,
|
||||
sockaddr.port,
|
||||
sockaddr.flowinfo,
|
||||
sockaddr.scope_id,
|
||||
)
|
||||
else:
|
||||
# IPv4
|
||||
sockaddr_tuple = (sockaddr.address, sockaddr.port)
|
||||
|
||||
res = res + r
|
||||
res.append(
|
||||
(
|
||||
addr_info.family,
|
||||
addr_info.type,
|
||||
addr_info.proto,
|
||||
"", # canonname
|
||||
sockaddr_tuple,
|
||||
)
|
||||
)
|
||||
|
||||
# Zeroconf tends to give us link-local IPv6 addresses without specifying
|
||||
# the link. Put those last in the list to be attempted.
|
||||
# Sort by preference
|
||||
res.sort(key=addr_preference_)
|
||||
return res
|
||||
|
||||
@@ -262,15 +255,7 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]:
|
||||
|
||||
# First "resolve" all the IP addresses to getaddrinfo() tuples of the form
|
||||
# (family, type, proto, canonname, sockaddr)
|
||||
res: list[
|
||||
tuple[
|
||||
int,
|
||||
int,
|
||||
int,
|
||||
str | None,
|
||||
tuple[str, int] | tuple[str, int, int, int],
|
||||
]
|
||||
] = []
|
||||
res: list[AddrInfo] = []
|
||||
for addr in address_list:
|
||||
# This should always work as these are supposed to be IP addresses
|
||||
try:
|
||||
|
||||
67
esphome/resolver.py
Normal file
67
esphome/resolver.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""DNS resolver for ESPHome using aioesphomeapi."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError
|
||||
import aioesphomeapi.host_resolver as hr
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
RESOLVE_TIMEOUT = 10.0 # seconds
|
||||
|
||||
|
||||
class AsyncResolver(threading.Thread):
|
||||
"""Resolver using aioesphomeapi that runs in a thread for faster results.
|
||||
|
||||
This resolver uses aioesphomeapi's async_resolve_host to handle DNS resolution,
|
||||
including proper .local domain fallback. Running in a thread allows us to get
|
||||
the result immediately without waiting for asyncio.run() to complete its
|
||||
cleanup cycle, which can take significant time.
|
||||
"""
|
||||
|
||||
def __init__(self, hosts: list[str], port: int) -> None:
|
||||
"""Initialize the resolver."""
|
||||
super().__init__(daemon=True)
|
||||
self.hosts = hosts
|
||||
self.port = port
|
||||
self.result: list[hr.AddrInfo] | None = None
|
||||
self.exception: Exception | None = None
|
||||
self.event = threading.Event()
|
||||
|
||||
async def _resolve(self) -> None:
|
||||
"""Resolve hostnames to IP addresses."""
|
||||
try:
|
||||
self.result = await hr.async_resolve_host(
|
||||
self.hosts, self.port, timeout=RESOLVE_TIMEOUT
|
||||
)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
# We need to catch all exceptions to ensure the event is set
|
||||
# Otherwise the thread could hang forever
|
||||
self.exception = e
|
||||
finally:
|
||||
self.event.set()
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the DNS resolution."""
|
||||
asyncio.run(self._resolve())
|
||||
|
||||
def resolve(self) -> list[hr.AddrInfo]:
|
||||
"""Start the thread and wait for the result."""
|
||||
self.start()
|
||||
|
||||
if not self.event.wait(
|
||||
timeout=RESOLVE_TIMEOUT + 1.0
|
||||
): # Give it 1 second more than the resolver timeout
|
||||
raise EsphomeError("Timeout resolving IP address")
|
||||
|
||||
if exc := self.exception:
|
||||
if isinstance(exc, ResolveTimeoutAPIError):
|
||||
raise EsphomeError(f"Timeout resolving IP address: {exc}") from exc
|
||||
if isinstance(exc, ResolveAPIError):
|
||||
raise EsphomeError(f"Error resolving IP address: {exc}") from exc
|
||||
raise exc
|
||||
|
||||
return self.result
|
||||
Reference in New Issue
Block a user