mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			usb_memory
			...
			light-addr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					902680a2e0 | ||
| 
						 | 
					1b3cbb9f60 | ||
| 
						 | 
					e3ecbf6d65 | ||
| 
						 | 
					603e3d94c7 | ||
| 
						 | 
					98f691913f | ||
| 
						 | 
					a89a35bff3 | ||
| 
						 | 
					e9e306501a | ||
| 
						 | 
					2aa3bceed8 | ||
| 
						 | 
					bdfa84ed87 | 
@@ -201,7 +201,6 @@ esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/hbridge/switch/* @dwmw2
 | 
			
		||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
 | 
			
		||||
esphome/components/he60r/* @clydebarrow
 | 
			
		||||
esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
 | 
			
		||||
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
 | 
			
		||||
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
 | 
			
		||||
 | 
			
		||||
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
void AdalightLightEffect::start() {
 | 
			
		||||
  AddressableLightEffect::start();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace adalight {
 | 
			
		||||
 | 
			
		||||
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  AdalightLightEffect(const char *name);
 | 
			
		||||
  AdalightLightEffect(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  if (light->supports_effects()) {
 | 
			
		||||
    msg.effects.emplace_back("None");
 | 
			
		||||
    for (auto *effect : light->get_effects()) {
 | 
			
		||||
      msg.effects.emplace_back(effect->get_name());
 | 
			
		||||
      msg.effects.push_back(effect->get_name());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
 | 
			
		||||
 
 | 
			
		||||
@@ -524,23 +524,13 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    call.set_target_humidity(this->target_humidity);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_fan_mode) {
 | 
			
		||||
    if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
 | 
			
		||||
      call.fan_mode_.reset();
 | 
			
		||||
      call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_fan_mode(this->fan_mode)) {
 | 
			
		||||
  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
    call.set_fan_mode(this->fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_preset) {
 | 
			
		||||
    if (this->custom_preset < traits.get_supported_custom_presets().size()) {
 | 
			
		||||
      call.preset_.reset();
 | 
			
		||||
      call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_preset(this->preset)) {
 | 
			
		||||
  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
    call.set_preset(this->preset);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
    call.set_swing_mode(this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  return call;
 | 
			
		||||
@@ -559,25 +549,41 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    climate->target_humidity = this->target_humidity;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_fan_mode) {
 | 
			
		||||
    if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
 | 
			
		||||
      climate->fan_mode.reset();
 | 
			
		||||
      climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_fan_mode(this->fan_mode)) {
 | 
			
		||||
  if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
 | 
			
		||||
    climate->fan_mode = this->fan_mode;
 | 
			
		||||
    climate->custom_fan_mode.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_preset) {
 | 
			
		||||
    if (this->custom_preset < traits.get_supported_custom_presets().size()) {
 | 
			
		||||
      climate->preset.reset();
 | 
			
		||||
      climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
 | 
			
		||||
    // std::set has consistent order (lexicographic for strings)
 | 
			
		||||
    const auto &modes = traits.get_supported_custom_fan_modes();
 | 
			
		||||
    if (custom_fan_mode < modes.size()) {
 | 
			
		||||
      size_t i = 0;
 | 
			
		||||
      for (const auto &mode : modes) {
 | 
			
		||||
        if (i == this->custom_fan_mode) {
 | 
			
		||||
          climate->custom_fan_mode = mode;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        i++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_preset(this->preset)) {
 | 
			
		||||
    climate->preset = this->preset;
 | 
			
		||||
    climate->custom_preset.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
  if (traits.get_supports_presets() && !this->uses_custom_preset) {
 | 
			
		||||
    climate->preset = this->preset;
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
 | 
			
		||||
    // std::set has consistent order (lexicographic for strings)
 | 
			
		||||
    const auto &presets = traits.get_supported_custom_presets();
 | 
			
		||||
    if (custom_preset < presets.size()) {
 | 
			
		||||
      size_t i = 0;
 | 
			
		||||
      for (const auto &preset : presets) {
 | 
			
		||||
        if (i == this->custom_preset) {
 | 
			
		||||
          climate->custom_preset = preset;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        i++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
    climate->swing_mode = this->swing_mode;
 | 
			
		||||
  }
 | 
			
		||||
  climate->publish_state();
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,6 @@ class Climate;
 | 
			
		||||
class ClimateCall {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ClimateCall(Climate *parent) : parent_(parent) {}
 | 
			
		||||
  friend struct ClimateDeviceRestoreState;
 | 
			
		||||
 | 
			
		||||
  /// Set the mode of the climate device.
 | 
			
		||||
  ClimateCall &set_mode(ClimateMode mode);
 | 
			
		||||
 
 | 
			
		||||
@@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
  ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
 | 
			
		||||
           light_effect->get_first_universe(), light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  light_effects_.insert(light_effect);
 | 
			
		||||
 | 
			
		||||
@@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
  ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
 | 
			
		||||
           light_effect->get_first_universe(), light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  light_effects_.erase(light_effect);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace e131 {
 | 
			
		||||
static const char *const TAG = "e131_addressable_light_effect";
 | 
			
		||||
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
 | 
			
		||||
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; }
 | 
			
		||||
 | 
			
		||||
@@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
 | 
			
		||||
      std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
 | 
			
		||||
  auto *input_data = packet.values + 1;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
 | 
			
		||||
           output_end);
 | 
			
		||||
  ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe,
 | 
			
		||||
           output_offset, output_end);
 | 
			
		||||
 | 
			
		||||
  switch (channels_) {
 | 
			
		||||
    case E131_MONO:
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 };
 | 
			
		||||
 | 
			
		||||
class E131AddressableLightEffect : public light::AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  E131AddressableLightEffect(const char *name);
 | 
			
		||||
  E131AddressableLightEffect(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -304,13 +304,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
 | 
			
		||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
 | 
			
		||||
    # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
 | 
			
		||||
    # a PIO platformio/framework-espidf value
 | 
			
		||||
    if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
 | 
			
		||||
        ext = "tar.xz"
 | 
			
		||||
    else:
 | 
			
		||||
        ext = "zip"
 | 
			
		||||
    if release:
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_framework_url(source: str) -> str:
 | 
			
		||||
@@ -359,7 +355,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 4, 3): cv.Version(55, 3, 32),
 | 
			
		||||
    cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
@@ -882,11 +877,6 @@ async def to_code(config):
 | 
			
		||||
    for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
 | 
			
		||||
        os.environ.pop(clean_var, None)
 | 
			
		||||
 | 
			
		||||
    # Set the location of the IDF component manager cache
 | 
			
		||||
    os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
 | 
			
		||||
        CORE.relative_internal_path(".espressif")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    add_extra_script(
 | 
			
		||||
        "post",
 | 
			
		||||
        "post_build.py",
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_MISO_PIN,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_MOSI_PIN,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    CONF_PAGE_ID,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_POLLING_INTERVAL,
 | 
			
		||||
@@ -53,36 +52,12 @@ from esphome.core import (
 | 
			
		||||
    coroutine_with_priority,
 | 
			
		||||
)
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CONFLICTS_WITH = ["wifi"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
AUTO_LOAD = ["network"]
 | 
			
		||||
LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
 | 
			
		||||
# These pins are used by the internal Ethernet MAC when using RMII PHYs
 | 
			
		||||
ESP32_RMII_FIXED_PINS = {
 | 
			
		||||
    19: "EMAC_TXD0",
 | 
			
		||||
    21: "EMAC_TX_EN",
 | 
			
		||||
    22: "EMAC_TXD1",
 | 
			
		||||
    25: "EMAC_RXD0",
 | 
			
		||||
    26: "EMAC_RXD1",
 | 
			
		||||
    27: "EMAC_RX_CRS_DV",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# RMII default pins for ESP32-P4
 | 
			
		||||
# These are the default pins used by ESP-IDF and are configurable in principle,
 | 
			
		||||
# but ESPHome's ethernet component currently has no way to change them
 | 
			
		||||
ESP32P4_RMII_DEFAULT_PINS = {
 | 
			
		||||
    34: "EMAC_TXD0",
 | 
			
		||||
    35: "EMAC_TXD1",
 | 
			
		||||
    28: "EMAC_RX_CRS_DV",
 | 
			
		||||
    29: "EMAC_RXD0",
 | 
			
		||||
    30: "EMAC_RXD1",
 | 
			
		||||
    49: "EMAC_TX_EN",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ethernet_ns = cg.esphome_ns.namespace("ethernet")
 | 
			
		||||
PHYRegister = ethernet_ns.struct("PHYRegister")
 | 
			
		||||
CONF_PHY_ADDR = "phy_addr"
 | 
			
		||||
@@ -298,7 +273,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate_spi(config):
 | 
			
		||||
def _final_validate(config):
 | 
			
		||||
    if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
 | 
			
		||||
        return
 | 
			
		||||
    if spi_configs := fv.full_config.get().get(CONF_SPI):
 | 
			
		||||
@@ -317,6 +292,9 @@ def _final_validate_spi(config):
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def manual_ip(config):
 | 
			
		||||
    return cg.StructInitializer(
 | 
			
		||||
        ManualIP,
 | 
			
		||||
@@ -405,57 +383,3 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    if CORE.using_arduino:
 | 
			
		||||
        cg.add_library("WiFi", None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate_rmii_pins(config: ConfigType) -> None:
 | 
			
		||||
    """Validate that RMII pins are not used by other components."""
 | 
			
		||||
    # Only validate for RMII-based PHYs on ESP32/ESP32P4
 | 
			
		||||
    if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
 | 
			
		||||
        return  # SPI and OPENETH don't use RMII
 | 
			
		||||
 | 
			
		||||
    variant = get_esp32_variant()
 | 
			
		||||
    if variant == VARIANT_ESP32:
 | 
			
		||||
        rmii_pins = ESP32_RMII_FIXED_PINS
 | 
			
		||||
        is_configurable = False
 | 
			
		||||
    elif variant == VARIANT_ESP32P4:
 | 
			
		||||
        rmii_pins = ESP32P4_RMII_DEFAULT_PINS
 | 
			
		||||
        is_configurable = True
 | 
			
		||||
    else:
 | 
			
		||||
        return  # No RMII validation needed for other variants
 | 
			
		||||
 | 
			
		||||
    # Check all used pins against RMII reserved pins
 | 
			
		||||
    for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
 | 
			
		||||
        for pin_path, _, pin_config in pin_list:
 | 
			
		||||
            pin_num = pin_config.get(CONF_NUMBER)
 | 
			
		||||
            if pin_num not in rmii_pins:
 | 
			
		||||
                continue
 | 
			
		||||
            # Found a conflict - show helpful error message
 | 
			
		||||
            pin_function = rmii_pins[pin_num]
 | 
			
		||||
            component_path = ".".join(str(p) for p in pin_path)
 | 
			
		||||
            if is_configurable:
 | 
			
		||||
                error_msg = (
 | 
			
		||||
                    f"GPIO{pin_num} is used by Ethernet RMII "
 | 
			
		||||
                    f"({pin_function}) with the current default "
 | 
			
		||||
                    f"configuration. This conflicts with '{component_path}'. "
 | 
			
		||||
                    f"Please choose a different GPIO pin for "
 | 
			
		||||
                    f"'{component_path}'."
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                error_msg = (
 | 
			
		||||
                    f"GPIO{pin_num} is reserved for Ethernet RMII "
 | 
			
		||||
                    f"({pin_function}) and cannot be used. This pin is "
 | 
			
		||||
                    f"hardcoded by ESP-IDF and cannot be changed when using "
 | 
			
		||||
                    f"RMII Ethernet PHYs. Please choose a different GPIO pin "
 | 
			
		||||
                    f"for '{component_path}'."
 | 
			
		||||
                )
 | 
			
		||||
            raise cv.Invalid(error_msg, path=pin_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Final validation for Ethernet component."""
 | 
			
		||||
    _final_validate_spi(config)
 | 
			
		||||
    _final_validate_rmii_pins(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@optimusprimespace", "@ssieb"]
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "hdc2010.h"
 | 
			
		||||
// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js
 | 
			
		||||
// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hdc2010 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "hdc2010";
 | 
			
		||||
 | 
			
		||||
static const uint8_t HDC2010_ADDRESS = 0x40;  // 0b1000000 or 0b1000001 from datasheet
 | 
			
		||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
 | 
			
		||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
 | 
			
		||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
 | 
			
		||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
 | 
			
		||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
 | 
			
		||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
 | 
			
		||||
static const uint8_t CONFIG = 0x0E;
 | 
			
		||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
 | 
			
		||||
  const uint8_t data[2] = {
 | 
			
		||||
      0b00000000,  // resolution 14bit for both humidity and temperature
 | 
			
		||||
      0b00000000   // reserved
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Initial config instruction error");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set measurement mode to temperature and humidity
 | 
			
		||||
  uint8_t config_contents;
 | 
			
		||||
  this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents = (config_contents & 0xF9);  // Always set to TEMP_AND_HUMID mode
 | 
			
		||||
  this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set rate to manual
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0x8F;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set temperature resolution to 14bit
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0x3F;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set humidity resolution to 14bit
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0xCF;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "HDC2010:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::update() {
 | 
			
		||||
  // Trigger measurement
 | 
			
		||||
  uint8_t config_contents;
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents |= 0x01;
 | 
			
		||||
  this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // 1ms delay after triggering the sample
 | 
			
		||||
  set_timeout(1, [this]() {
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
      float temp = this->read_temp();
 | 
			
		||||
      this->temperature_sensor_->publish_state(temp);
 | 
			
		||||
      ESP_LOGD(TAG, "Temp=%.1f°C", temp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
      float humidity = this->read_humidity();
 | 
			
		||||
      this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
      ESP_LOGD(TAG, "Humidity=%.1f%%", humidity);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float HDC2010Component::read_temp() {
 | 
			
		||||
  uint8_t byte[2];
 | 
			
		||||
 | 
			
		||||
  this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
 | 
			
		||||
  this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
 | 
			
		||||
 | 
			
		||||
  uint16_t temp = encode_uint16(byte[1], byte[0]);
 | 
			
		||||
  return (float) temp * 0.0025177f - 40.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float HDC2010Component::read_humidity() {
 | 
			
		||||
  uint8_t byte[2];
 | 
			
		||||
 | 
			
		||||
  this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
 | 
			
		||||
  this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
 | 
			
		||||
 | 
			
		||||
  uint16_t humidity = encode_uint16(byte[1], byte[0]);
 | 
			
		||||
  return (float) humidity * 0.001525879f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace hdc2010
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hdc2010 {
 | 
			
		||||
 | 
			
		||||
class HDC2010Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
 | 
			
		||||
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
 | 
			
		||||
 | 
			
		||||
  /// Setup the sensor and check for connection.
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// Retrieve the latest sensor values. This operation takes approximately 16ms.
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  float read_temp();
 | 
			
		||||
 | 
			
		||||
  float read_humidity();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace hdc2010
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,56 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
hdc2010_ns = cg.esphome_ns.namespace("hdc2010")
 | 
			
		||||
HDC2010Component = hdc2010_ns.class_(
 | 
			
		||||
    "HDC2010Component", cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(HDC2010Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x40))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if temperature_config := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(temperature_config)
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if humidity_config := config.get(CONF_HUMIDITY):
 | 
			
		||||
        sens = await sensor.new_sensor(humidity_config)
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
@@ -169,7 +169,7 @@ class HttpRequestComponent : public Component {
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
 | 
			
		||||
                                                 const std::string &body, const std::list<Header> &request_headers,
 | 
			
		||||
                                                 const std::set<std::string> &collect_headers) = 0;
 | 
			
		||||
                                                 std::set<std::string> collect_headers) = 0;
 | 
			
		||||
  const char *useragent_{nullptr};
 | 
			
		||||
  bool follow_redirects_{};
 | 
			
		||||
  uint16_t redirect_limit_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino";
 | 
			
		||||
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
 | 
			
		||||
                                                           const std::string &body,
 | 
			
		||||
                                                           const std::list<Header> &request_headers,
 | 
			
		||||
                                                           const std::set<std::string> &collect_headers) {
 | 
			
		||||
                                                           std::set<std::string> collect_headers) {
 | 
			
		||||
  if (!network::is_connected()) {
 | 
			
		||||
    this->status_momentary_error("failed", 1000);
 | 
			
		||||
    ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent {
 | 
			
		||||
 protected:
 | 
			
		||||
  std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
 | 
			
		||||
                                         const std::list<Header> &request_headers,
 | 
			
		||||
                                         const std::set<std::string> &collect_headers) override;
 | 
			
		||||
                                         std::set<std::string> collect_headers) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace http_request
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host";
 | 
			
		||||
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
 | 
			
		||||
                                                        const std::string &body,
 | 
			
		||||
                                                        const std::list<Header> &request_headers,
 | 
			
		||||
                                                        const std::set<std::string> &response_headers) {
 | 
			
		||||
                                                        std::set<std::string> response_headers) {
 | 
			
		||||
  if (!network::is_connected()) {
 | 
			
		||||
    this->status_momentary_error("failed", 1000);
 | 
			
		||||
    ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
 | 
			
		||||
                                         const std::list<Header> &request_headers,
 | 
			
		||||
                                         const std::set<std::string> &response_headers) override;
 | 
			
		||||
                                         std::set<std::string> response_headers) override;
 | 
			
		||||
  void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
 | 
			
		||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
 | 
			
		||||
                                                       const std::string &body,
 | 
			
		||||
                                                       const std::list<Header> &request_headers,
 | 
			
		||||
                                                       const std::set<std::string> &collect_headers) {
 | 
			
		||||
                                                       std::set<std::string> collect_headers) {
 | 
			
		||||
  if (!network::is_connected()) {
 | 
			
		||||
    this->status_momentary_error("failed", 1000);
 | 
			
		||||
    ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent {
 | 
			
		||||
 protected:
 | 
			
		||||
  std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
 | 
			
		||||
                                         const std::list<Header> &request_headers,
 | 
			
		||||
                                         const std::set<std::string> &collect_headers) override;
 | 
			
		||||
                                         std::set<std::string> collect_headers) override;
 | 
			
		||||
  // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
 | 
			
		||||
  uint16_t buffer_size_rx_{};
 | 
			
		||||
  uint16_t buffer_size_tx_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -28,38 +28,6 @@ void ImprovSerialComponent::setup() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::loop() {
 | 
			
		||||
  if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
 | 
			
		||||
    this->last_read_byte_ = 0;
 | 
			
		||||
    this->rx_buffer_.clear();
 | 
			
		||||
    ESP_LOGV(TAG, "Timeout");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto byte = this->read_byte_();
 | 
			
		||||
  while (byte.has_value()) {
 | 
			
		||||
    if (this->parse_improv_serial_byte_(byte.value())) {
 | 
			
		||||
      this->last_read_byte_ = millis();
 | 
			
		||||
    } else {
 | 
			
		||||
      this->last_read_byte_ = 0;
 | 
			
		||||
      this->rx_buffer_.clear();
 | 
			
		||||
    }
 | 
			
		||||
    byte = this->read_byte_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == improv::STATE_PROVISIONING) {
 | 
			
		||||
    if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
      wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
 | 
			
		||||
                                                 this->connecting_sta_.get_password());
 | 
			
		||||
      this->connecting_sta_ = {};
 | 
			
		||||
      this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
      this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
 | 
			
		||||
      std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
 | 
			
		||||
      this->send_response_(url);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
 | 
			
		||||
 | 
			
		||||
optional<uint8_t> ImprovSerialComponent::read_byte_() {
 | 
			
		||||
@@ -110,28 +78,8 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
 | 
			
		||||
  return byte;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
 | 
			
		||||
  // First, set length field
 | 
			
		||||
  this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
 | 
			
		||||
 | 
			
		||||
  const bool there_is_data = data != nullptr && size > 0;
 | 
			
		||||
  // If there_is_data, checksum must not include our optional data byte
 | 
			
		||||
  const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
 | 
			
		||||
  // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
 | 
			
		||||
  const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
 | 
			
		||||
  // Calculate checksum for message
 | 
			
		||||
  uint8_t checksum = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < header_checksum_len; i++) {
 | 
			
		||||
    checksum += this->tx_header_[i];
 | 
			
		||||
  }
 | 
			
		||||
  if (there_is_data) {
 | 
			
		||||
    // Include data in checksum
 | 
			
		||||
    for (size_t i = 0; i < size; i++) {
 | 
			
		||||
      checksum += data[i];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->tx_header_[TX_CHECKSUM_IDX] = checksum;
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
 | 
			
		||||
  data.push_back('\n');
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  switch (logger::global_logger->get_uart()) {
 | 
			
		||||
    case logger::UART_SELECTION_UART0:
 | 
			
		||||
@@ -139,45 +87,63 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
 | 
			
		||||
    !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
    case logger::UART_SELECTION_UART2:
 | 
			
		||||
#endif
 | 
			
		||||
      uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
 | 
			
		||||
      if (there_is_data) {
 | 
			
		||||
        uart_write_bytes(this->uart_num_, data, size);
 | 
			
		||||
        uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline
 | 
			
		||||
      }
 | 
			
		||||
#endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
      uart_write_bytes(this->uart_num_, data.data(), data.size());
 | 
			
		||||
      break;
 | 
			
		||||
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
 | 
			
		||||
    case logger::UART_SELECTION_USB_CDC:
 | 
			
		||||
      esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
 | 
			
		||||
      if (there_is_data) {
 | 
			
		||||
        esp_usb_console_write_buf((const char *) data, size);
 | 
			
		||||
        esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
 | 
			
		||||
                                  2);  // Footer: checksum and newline
 | 
			
		||||
      }
 | 
			
		||||
    case logger::UART_SELECTION_USB_CDC: {
 | 
			
		||||
      const char *msg = (char *) data.data();
 | 
			
		||||
      esp_usb_console_write_buf(msg, data.size());
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
#endif  // USE_LOGGER_USB_CDC
 | 
			
		||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
 | 
			
		||||
    case logger::UART_SELECTION_USB_SERIAL_JTAG:
 | 
			
		||||
      usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
 | 
			
		||||
      if (there_is_data) {
 | 
			
		||||
        usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
 | 
			
		||||
        usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
 | 
			
		||||
                                    20 / portTICK_PERIOD_MS);  // Footer: checksum and newline
 | 
			
		||||
      }
 | 
			
		||||
      usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS);
 | 
			
		||||
      delay(10);
 | 
			
		||||
      usb_serial_jtag_ll_txfifo_flush();  // fixes for issue in IDF 4.4.7
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_LOGGER_USB_SERIAL_JTAG
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
#elif defined(USE_ARDUINO)
 | 
			
		||||
  this->hw_serial_->write(this->tx_header_, header_tx_len);
 | 
			
		||||
  if (there_is_data) {
 | 
			
		||||
    this->hw_serial_->write(data, size);
 | 
			
		||||
    this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline
 | 
			
		||||
  }
 | 
			
		||||
  this->hw_serial_->write(data.data(), data.size());
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::loop() {
 | 
			
		||||
  if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
 | 
			
		||||
    this->last_read_byte_ = 0;
 | 
			
		||||
    this->rx_buffer_.clear();
 | 
			
		||||
    ESP_LOGV(TAG, "Improv Serial timeout");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto byte = this->read_byte_();
 | 
			
		||||
  while (byte.has_value()) {
 | 
			
		||||
    if (this->parse_improv_serial_byte_(byte.value())) {
 | 
			
		||||
      this->last_read_byte_ = millis();
 | 
			
		||||
    } else {
 | 
			
		||||
      this->last_read_byte_ = 0;
 | 
			
		||||
      this->rx_buffer_.clear();
 | 
			
		||||
    }
 | 
			
		||||
    byte = this->read_byte_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == improv::STATE_PROVISIONING) {
 | 
			
		||||
    if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
      wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
 | 
			
		||||
                                                 this->connecting_sta_.get_password());
 | 
			
		||||
      this->connecting_sta_ = {};
 | 
			
		||||
      this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
      this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
 | 
			
		||||
      std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
 | 
			
		||||
      this->send_response_(url);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
 | 
			
		||||
  std::vector<std::string> urls;
 | 
			
		||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
 | 
			
		||||
@@ -211,13 +177,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() {
 | 
			
		||||
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
 | 
			
		||||
  size_t at = this->rx_buffer_.size();
 | 
			
		||||
  this->rx_buffer_.push_back(byte);
 | 
			
		||||
  ESP_LOGV(TAG, "Byte: 0x%02X", byte);
 | 
			
		||||
  ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte);
 | 
			
		||||
  const uint8_t *raw = &this->rx_buffer_[0];
 | 
			
		||||
 | 
			
		||||
  return improv::parse_improv_serial_byte(
 | 
			
		||||
      at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
 | 
			
		||||
      [this](improv::Error error) -> void {
 | 
			
		||||
        ESP_LOGW(TAG, "Error decoding payload");
 | 
			
		||||
        ESP_LOGW(TAG, "Error decoding Improv payload");
 | 
			
		||||
        this->set_error_(error);
 | 
			
		||||
      });
 | 
			
		||||
}
 | 
			
		||||
@@ -233,7 +199,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
 | 
			
		||||
      wifi::global_wifi_component->set_sta(sta);
 | 
			
		||||
      wifi::global_wifi_component->start_connecting(sta, false);
 | 
			
		||||
      this->set_state_(improv::STATE_PROVISIONING);
 | 
			
		||||
      ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
 | 
			
		||||
      ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
 | 
			
		||||
               command.password.c_str());
 | 
			
		||||
 | 
			
		||||
      auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
 | 
			
		||||
@@ -274,7 +240,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
      ESP_LOGW(TAG, "Unknown payload");
 | 
			
		||||
      ESP_LOGW(TAG, "Unknown Improv payload");
 | 
			
		||||
      this->set_error_(improv::ERROR_UNKNOWN_RPC);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
@@ -283,26 +249,57 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::set_state_(improv::State state) {
 | 
			
		||||
  this->state_ = state;
 | 
			
		||||
  this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
 | 
			
		||||
  this->tx_header_[TX_DATA_IDX] = state;
 | 
			
		||||
  this->write_data_();
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
 | 
			
		||||
  data.resize(11);
 | 
			
		||||
  data[6] = IMPROV_SERIAL_VERSION;
 | 
			
		||||
  data[7] = TYPE_CURRENT_STATE;
 | 
			
		||||
  data[8] = 1;
 | 
			
		||||
  data[9] = state;
 | 
			
		||||
 | 
			
		||||
  uint8_t checksum = 0x00;
 | 
			
		||||
  for (uint8_t d : data)
 | 
			
		||||
    checksum += d;
 | 
			
		||||
  data[10] = checksum;
 | 
			
		||||
 | 
			
		||||
  this->write_data_(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::set_error_(improv::Error error) {
 | 
			
		||||
  this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
 | 
			
		||||
  this->tx_header_[TX_DATA_IDX] = error;
 | 
			
		||||
  this->write_data_();
 | 
			
		||||
  std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
 | 
			
		||||
  data.resize(11);
 | 
			
		||||
  data[6] = IMPROV_SERIAL_VERSION;
 | 
			
		||||
  data[7] = TYPE_ERROR_STATE;
 | 
			
		||||
  data[8] = 1;
 | 
			
		||||
  data[9] = error;
 | 
			
		||||
 | 
			
		||||
  uint8_t checksum = 0x00;
 | 
			
		||||
  for (uint8_t d : data)
 | 
			
		||||
    checksum += d;
 | 
			
		||||
  data[10] = checksum;
 | 
			
		||||
  this->write_data_(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
 | 
			
		||||
  this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
 | 
			
		||||
  this->write_data_(response.data(), response.size());
 | 
			
		||||
  std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
 | 
			
		||||
  data.resize(9);
 | 
			
		||||
  data[6] = IMPROV_SERIAL_VERSION;
 | 
			
		||||
  data[7] = TYPE_RPC_RESPONSE;
 | 
			
		||||
  data[8] = response.size();
 | 
			
		||||
  data.insert(data.end(), response.begin(), response.end());
 | 
			
		||||
 | 
			
		||||
  uint8_t checksum = 0x00;
 | 
			
		||||
  for (uint8_t d : data)
 | 
			
		||||
    checksum += d;
 | 
			
		||||
  data.push_back(checksum);
 | 
			
		||||
 | 
			
		||||
  this->write_data_(data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ImprovSerialComponent::on_wifi_connect_timeout_() {
 | 
			
		||||
  this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
 | 
			
		||||
  this->set_state_(improv::STATE_AUTHORIZED);
 | 
			
		||||
  ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
 | 
			
		||||
  ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
 | 
			
		||||
  wifi::global_wifi_component->clear_sta();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,16 +26,6 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace improv_serial {
 | 
			
		||||
 | 
			
		||||
// TX buffer layout constants
 | 
			
		||||
static constexpr uint8_t TX_HEADER_SIZE = 6;  // Bytes 0-5 = "IMPROV"
 | 
			
		||||
static constexpr uint8_t TX_VERSION_IDX = 6;
 | 
			
		||||
static constexpr uint8_t TX_TYPE_IDX = 7;
 | 
			
		||||
static constexpr uint8_t TX_LENGTH_IDX = 8;
 | 
			
		||||
static constexpr uint8_t TX_DATA_IDX = 9;  // For state/error messages only
 | 
			
		||||
static constexpr uint8_t TX_CHECKSUM_IDX = 10;
 | 
			
		||||
static constexpr uint8_t TX_NEWLINE_IDX = 11;
 | 
			
		||||
static constexpr uint8_t TX_BUFFER_SIZE = 12;
 | 
			
		||||
 | 
			
		||||
enum ImprovSerialType : uint8_t {
 | 
			
		||||
  TYPE_CURRENT_STATE = 0x01,
 | 
			
		||||
  TYPE_ERROR_STATE = 0x02,
 | 
			
		||||
@@ -67,22 +57,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
 | 
			
		||||
  std::vector<uint8_t> build_version_info_();
 | 
			
		||||
 | 
			
		||||
  optional<uint8_t> read_byte_();
 | 
			
		||||
  void write_data_(const uint8_t *data = nullptr, size_t size = 0);
 | 
			
		||||
 | 
			
		||||
  uint8_t tx_header_[TX_BUFFER_SIZE] = {
 | 
			
		||||
      'I',                    // 0: Header
 | 
			
		||||
      'M',                    // 1: Header
 | 
			
		||||
      'P',                    // 2: Header
 | 
			
		||||
      'R',                    // 3: Header
 | 
			
		||||
      'O',                    // 4: Header
 | 
			
		||||
      'V',                    // 5: Header
 | 
			
		||||
      IMPROV_SERIAL_VERSION,  // 6: Version
 | 
			
		||||
      0,                      // 7: ImprovSerialType
 | 
			
		||||
      0,                      // 8: Length
 | 
			
		||||
      0,                      // 9...X: Data (here, one byte reserved for state/error)
 | 
			
		||||
      0,                      // X + 10: Checksum
 | 
			
		||||
      '\n',
 | 
			
		||||
  };
 | 
			
		||||
  void write_data_(std::vector<uint8_t> &data);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  uart_port_t uart_num_;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u)
 | 
			
		||||
 | 
			
		||||
class AddressableLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
  void start_internal() override {
 | 
			
		||||
    this->get_addressable_()->set_effect_active(true);
 | 
			
		||||
    this->get_addressable_()->clear_effect_data();
 | 
			
		||||
@@ -57,7 +57,8 @@ class AddressableLightEffect : public LightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableLambdaLightEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f,
 | 
			
		||||
  AddressableLambdaLightEffect(const std::string &name,
 | 
			
		||||
                               std::function<void(AddressableLight &, Color, bool initial_run)> f,
 | 
			
		||||
                               uint32_t update_interval)
 | 
			
		||||
      : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
 | 
			
		||||
  void start() override { this->initial_run_ = true; }
 | 
			
		||||
@@ -80,7 +81,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableRainbowLightEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void apply(AddressableLight &it, const Color ¤t_color) override {
 | 
			
		||||
    ESPHSVColor hsv;
 | 
			
		||||
    hsv.value = 255;
 | 
			
		||||
@@ -111,7 +112,7 @@ struct AddressableColorWipeEffectColor {
 | 
			
		||||
 | 
			
		||||
class AddressableColorWipeEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; }
 | 
			
		||||
  void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; }
 | 
			
		||||
  void set_reverse(bool reverse) { this->reverse_ = reverse; }
 | 
			
		||||
@@ -164,7 +165,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableScanEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
 | 
			
		||||
  void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
 | 
			
		||||
  void apply(AddressableLight &it, const Color ¤t_color) override {
 | 
			
		||||
@@ -201,7 +202,7 @@ class AddressableScanEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void apply(AddressableLight &addressable, const Color ¤t_color) override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    uint8_t pos_add = 0;
 | 
			
		||||
@@ -243,7 +244,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableRandomTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void apply(AddressableLight &it, const Color ¤t_color) override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    uint8_t pos_add = 0;
 | 
			
		||||
@@ -292,7 +293,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableFireworksEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void start() override {
 | 
			
		||||
    auto &it = *this->get_addressable_();
 | 
			
		||||
    it.all() = Color::BLACK;
 | 
			
		||||
@@ -341,7 +342,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
 | 
			
		||||
 | 
			
		||||
class AddressableFlickerEffect : public AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
  explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
  void apply(AddressableLight &it, const Color ¤t_color) override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    const uint8_t intensity = this->intensity_;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ inline static float random_cubic_float() {
 | 
			
		||||
/// Pulse effect.
 | 
			
		||||
class PulseLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit PulseLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  explicit PulseLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
  void apply() override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
@@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect {
 | 
			
		||||
/// Random effect. Sets random colors every 10 seconds and slowly transitions between them.
 | 
			
		||||
class RandomLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit RandomLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  explicit RandomLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
  void apply() override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
@@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect {
 | 
			
		||||
 | 
			
		||||
class LambdaLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval)
 | 
			
		||||
  LambdaLightEffect(const std::string &name, std::function<void(bool initial_run)> f, uint32_t update_interval)
 | 
			
		||||
      : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
 | 
			
		||||
 | 
			
		||||
  void start() override { this->initial_run_ = true; }
 | 
			
		||||
@@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect {
 | 
			
		||||
 | 
			
		||||
class AutomationLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  AutomationLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  AutomationLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
  void stop() override { this->trig_->stop_action(); }
 | 
			
		||||
  void apply() override {
 | 
			
		||||
    if (!this->trig_->is_action_running()) {
 | 
			
		||||
@@ -163,7 +163,7 @@ struct StrobeLightEffectColor {
 | 
			
		||||
 | 
			
		||||
class StrobeLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit StrobeLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
  void apply() override {
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    if (now - this->last_switch_ < this->colors_[this->at_color_].duration)
 | 
			
		||||
@@ -198,7 +198,7 @@ class StrobeLightEffect : public LightEffect {
 | 
			
		||||
 | 
			
		||||
class FlickerLightEffect : public LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FlickerLightEffect(const char *name) : LightEffect(name) {}
 | 
			
		||||
  explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
  void apply() override {
 | 
			
		||||
    LightColorValues remote = this->state_->remote_values;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include "esphome/core/finite_set_mask.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace light {
 | 
			
		||||
@@ -108,9 +107,13 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
 | 
			
		||||
// Type alias for raw color mode bitmask values
 | 
			
		||||
using color_mode_bitmask_t = uint16_t;
 | 
			
		||||
 | 
			
		||||
// Lookup table for ColorMode bit mapping
 | 
			
		||||
// This array defines the canonical order of color modes (bit 0-9)
 | 
			
		||||
constexpr ColorMode COLOR_MODE_LOOKUP[] = {
 | 
			
		||||
// Constants for ColorMode count and bit range
 | 
			
		||||
static constexpr int COLOR_MODE_COUNT = 10;                             // UNKNOWN through RGB_COLD_WARM_WHITE
 | 
			
		||||
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8;  // Number of bits in bitmask type
 | 
			
		||||
 | 
			
		||||
// Compile-time array of all ColorMode values in declaration order
 | 
			
		||||
// Bit positions (0-9) map directly to enum declaration order
 | 
			
		||||
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
 | 
			
		||||
    ColorMode::UNKNOWN,                // bit 0
 | 
			
		||||
    ColorMode::ON_OFF,                 // bit 1
 | 
			
		||||
    ColorMode::BRIGHTNESS,             // bit 2
 | 
			
		||||
@@ -123,42 +126,33 @@ constexpr ColorMode COLOR_MODE_LOOKUP[] = {
 | 
			
		||||
    ColorMode::RGB_COLD_WARM_WHITE,    // bit 9
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Bit mapping policy for ColorMode
 | 
			
		||||
/// Uses lookup table for non-contiguous enum values
 | 
			
		||||
struct ColorModeBitPolicy {
 | 
			
		||||
  using mask_t = uint16_t;  // 10 bits requires uint16_t
 | 
			
		||||
  static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
 | 
			
		||||
 | 
			
		||||
  static constexpr unsigned to_bit(ColorMode mode) {
 | 
			
		||||
    // Linear search through lookup table
 | 
			
		||||
    // Compiler optimizes this to efficient code since array is constexpr
 | 
			
		||||
    for (int i = 0; i < MAX_BITS; ++i) {
 | 
			
		||||
      if (COLOR_MODE_LOOKUP[i] == mode)
 | 
			
		||||
        return i;
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
/// Map ColorMode enum values to bit positions (0-9)
 | 
			
		||||
/// Bit positions follow the enum declaration order
 | 
			
		||||
static constexpr int mode_to_bit(ColorMode mode) {
 | 
			
		||||
  // Linear search through COLOR_MODES array
 | 
			
		||||
  // Compiler optimizes this to efficient code since array is constexpr
 | 
			
		||||
  for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
 | 
			
		||||
    if (COLOR_MODES[i] == mode)
 | 
			
		||||
      return i;
 | 
			
		||||
  }
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  static constexpr ColorMode from_bit(unsigned bit) {
 | 
			
		||||
    return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Type alias for ColorMode bitmask using policy-based design
 | 
			
		||||
using ColorModeMask = FiniteSetMask<ColorMode, ColorModeBitPolicy>;
 | 
			
		||||
 | 
			
		||||
// Number of ColorCapability enum values
 | 
			
		||||
constexpr int COLOR_CAPABILITY_COUNT = 6;
 | 
			
		||||
/// Map bit positions (0-9) to ColorMode enum values
 | 
			
		||||
/// Bit positions follow the enum declaration order
 | 
			
		||||
static constexpr ColorMode bit_to_mode(int bit) {
 | 
			
		||||
  // Direct lookup in COLOR_MODES array
 | 
			
		||||
  return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper to compute capability bitmask at compile time
 | 
			
		||||
constexpr uint16_t compute_capability_bitmask(ColorCapability capability) {
 | 
			
		||||
  uint16_t mask = 0;
 | 
			
		||||
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
 | 
			
		||||
  color_mode_bitmask_t mask = 0;
 | 
			
		||||
  uint8_t cap_bit = static_cast<uint8_t>(capability);
 | 
			
		||||
 | 
			
		||||
  // Check each ColorMode to see if it has this capability
 | 
			
		||||
  constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
 | 
			
		||||
  for (int bit = 0; bit < color_mode_count; ++bit) {
 | 
			
		||||
    uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]);
 | 
			
		||||
  for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
 | 
			
		||||
    uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
 | 
			
		||||
    if ((mode_val & cap_bit) != 0) {
 | 
			
		||||
      mask |= (1 << bit);
 | 
			
		||||
    }
 | 
			
		||||
@@ -166,9 +160,12 @@ constexpr uint16_t compute_capability_bitmask(ColorCapability capability) {
 | 
			
		||||
  return mask;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Number of ColorCapability enum values
 | 
			
		||||
static constexpr int COLOR_CAPABILITY_COUNT = 6;
 | 
			
		||||
 | 
			
		||||
/// Compile-time lookup table mapping ColorCapability to bitmask
 | 
			
		||||
/// This array is computed at compile time using constexpr
 | 
			
		||||
constexpr uint16_t CAPABILITY_BITMASKS[] = {
 | 
			
		||||
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::ON_OFF),             // 1 << 0
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::BRIGHTNESS),         // 1 << 1
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::WHITE),              // 1 << 2
 | 
			
		||||
@@ -177,38 +174,130 @@ constexpr uint16_t CAPABILITY_BITMASKS[] = {
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::RGB),                // 1 << 5
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS
 | 
			
		||||
 * lookup.
 | 
			
		||||
 *
 | 
			
		||||
 * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5).
 | 
			
		||||
 * Used to index into the CAPABILITY_BITMASKS lookup table.
 | 
			
		||||
 *
 | 
			
		||||
 * @param capability A ColorCapability enum value (must be a power of 2).
 | 
			
		||||
 * @return The corresponding array index (0-based).
 | 
			
		||||
 */
 | 
			
		||||
inline int capability_to_index(ColorCapability capability) {
 | 
			
		||||
  uint8_t cap_val = static_cast<uint8_t>(capability);
 | 
			
		||||
#if defined(__GNUC__) || defined(__clang__)
 | 
			
		||||
  // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
 | 
			
		||||
  return __builtin_ctz(cap_val);
 | 
			
		||||
#else
 | 
			
		||||
  // Fallback for compilers without __builtin_ctz
 | 
			
		||||
  int index = 0;
 | 
			
		||||
  while (cap_val > 1) {
 | 
			
		||||
    cap_val >>= 1;
 | 
			
		||||
    ++index;
 | 
			
		||||
  }
 | 
			
		||||
  return index;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
/// Bitmask for storing a set of ColorMode values efficiently.
 | 
			
		||||
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
 | 
			
		||||
class ColorModeMask {
 | 
			
		||||
 public:
 | 
			
		||||
  constexpr ColorModeMask() = default;
 | 
			
		||||
 | 
			
		||||
/// Check if any mode in the bitmask has a specific capability
 | 
			
		||||
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
 | 
			
		||||
inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) {
 | 
			
		||||
  // Lookup the pre-computed bitmask for this capability and check intersection with our mask
 | 
			
		||||
  return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0;
 | 
			
		||||
}
 | 
			
		||||
  /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
 | 
			
		||||
  constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
 | 
			
		||||
    for (auto mode : modes) {
 | 
			
		||||
      this->add(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
 | 
			
		||||
 | 
			
		||||
  /// Add multiple modes at once using initializer list
 | 
			
		||||
  constexpr void add(std::initializer_list<ColorMode> modes) {
 | 
			
		||||
    for (auto mode : modes) {
 | 
			
		||||
      this->add(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
 | 
			
		||||
 | 
			
		||||
  constexpr size_t size() const {
 | 
			
		||||
    // Count set bits using Brian Kernighan's algorithm
 | 
			
		||||
    // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
 | 
			
		||||
    uint16_t n = this->mask_;
 | 
			
		||||
    size_t count = 0;
 | 
			
		||||
    while (n) {
 | 
			
		||||
      n &= n - 1;  // Clear the least significant set bit
 | 
			
		||||
      count++;
 | 
			
		||||
    }
 | 
			
		||||
    return count;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr bool empty() const { return this->mask_ == 0; }
 | 
			
		||||
 | 
			
		||||
  /// Iterator support for API encoding
 | 
			
		||||
  class Iterator {
 | 
			
		||||
   public:
 | 
			
		||||
    using iterator_category = std::forward_iterator_tag;
 | 
			
		||||
    using value_type = ColorMode;
 | 
			
		||||
    using difference_type = std::ptrdiff_t;
 | 
			
		||||
    using pointer = const ColorMode *;
 | 
			
		||||
    using reference = ColorMode;
 | 
			
		||||
 | 
			
		||||
    constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
 | 
			
		||||
 | 
			
		||||
    constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
 | 
			
		||||
 | 
			
		||||
    constexpr Iterator &operator++() {
 | 
			
		||||
      ++bit_;
 | 
			
		||||
      advance_to_next_set_bit_();
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
 | 
			
		||||
 | 
			
		||||
   private:
 | 
			
		||||
    constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
 | 
			
		||||
 | 
			
		||||
    color_mode_bitmask_t mask_;
 | 
			
		||||
    int bit_;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constexpr Iterator begin() const { return Iterator(mask_, 0); }
 | 
			
		||||
  constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
 | 
			
		||||
 | 
			
		||||
  /// Get the raw bitmask value for API encoding
 | 
			
		||||
  constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
 | 
			
		||||
 | 
			
		||||
  /// Find the next set bit in a bitmask starting from a given position
 | 
			
		||||
  /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
 | 
			
		||||
  static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
 | 
			
		||||
    int bit = start_bit;
 | 
			
		||||
    while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
 | 
			
		||||
      ++bit;
 | 
			
		||||
    }
 | 
			
		||||
    return bit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Find the first set bit in a bitmask and return the corresponding ColorMode
 | 
			
		||||
  /// Used for optimizing compute_color_mode_() intersection logic
 | 
			
		||||
  static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
 | 
			
		||||
    return bit_to_mode(find_next_set_bit(mask, 0));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Check if a ColorMode is present in a raw bitmask value
 | 
			
		||||
  /// Useful for checking intersection results without creating a temporary ColorModeMask
 | 
			
		||||
  static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
 | 
			
		||||
    return (mask & (1 << mode_to_bit(mode))) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Check if any mode in the bitmask has a specific capability
 | 
			
		||||
  /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
 | 
			
		||||
  bool has_capability(ColorCapability capability) const {
 | 
			
		||||
    // Lookup the pre-computed bitmask for this capability and check intersection with our mask
 | 
			
		||||
    // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
 | 
			
		||||
    // We need to convert the power-of-2 value to an index
 | 
			
		||||
    uint8_t cap_val = static_cast<uint8_t>(capability);
 | 
			
		||||
#if defined(__GNUC__) || defined(__clang__)
 | 
			
		||||
    // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
 | 
			
		||||
    int index = __builtin_ctz(cap_val);
 | 
			
		||||
#else
 | 
			
		||||
    // Fallback for compilers without __builtin_ctz
 | 
			
		||||
    int index = 0;
 | 
			
		||||
    while (cap_val > 1) {
 | 
			
		||||
      cap_val >>= 1;
 | 
			
		||||
      ++index;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
 | 
			
		||||
  // Currently only 10 ColorMode values exist, so 16 bits is sufficient.
 | 
			
		||||
  // Can be changed to uint32_t if more than 16 color modes are needed in the future.
 | 
			
		||||
  // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
 | 
			
		||||
  color_mode_bitmask_t mask_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -156,7 +156,7 @@ void LightCall::perform() {
 | 
			
		||||
    if (this->effect_ == 0u) {
 | 
			
		||||
      effect_s = "None";
 | 
			
		||||
    } else {
 | 
			
		||||
      effect_s = this->parent_->effects_[this->effect_ - 1]->get_name();
 | 
			
		||||
      effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (publish) {
 | 
			
		||||
@@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
 | 
			
		||||
  // Use the preferred suitable mode.
 | 
			
		||||
  if (intersection != 0) {
 | 
			
		||||
    ColorMode mode = ColorModeMask::first_value_from_mask(intersection);
 | 
			
		||||
    ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
 | 
			
		||||
    ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
 | 
			
		||||
             LOG_STR_ARG(color_mode_to_human(mode)));
 | 
			
		||||
    return mode;
 | 
			
		||||
@@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) {
 | 
			
		||||
  for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) {
 | 
			
		||||
    LightEffect *e = this->parent_->effects_[i];
 | 
			
		||||
 | 
			
		||||
    if (strcasecmp(effect.c_str(), e->get_name()) == 0) {
 | 
			
		||||
    if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) {
 | 
			
		||||
      this->set_effect(i + 1);
 | 
			
		||||
      found = true;
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -9,7 +11,7 @@ class LightState;
 | 
			
		||||
 | 
			
		||||
class LightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit LightEffect(const char *name) : name_(name) {}
 | 
			
		||||
  explicit LightEffect(std::string name) : name_(std::move(name)) {}
 | 
			
		||||
 | 
			
		||||
  /// Initialize this LightEffect. Will be called once after creation.
 | 
			
		||||
  virtual void start() {}
 | 
			
		||||
@@ -22,11 +24,7 @@ class LightEffect {
 | 
			
		||||
  /// Apply this effect. Use the provided state for starting transitions, ...
 | 
			
		||||
  virtual void apply() = 0;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the name of this effect.
 | 
			
		||||
   * The returned pointer is valid for the lifetime of the program and must not be freed.
 | 
			
		||||
   */
 | 
			
		||||
  const char *get_name() const { return this->name_; }
 | 
			
		||||
  const std::string &get_name() { return this->name_; }
 | 
			
		||||
 | 
			
		||||
  /// Internal method called by the LightState when this light effect is registered in it.
 | 
			
		||||
  virtual void init() {}
 | 
			
		||||
@@ -49,7 +47,7 @@ class LightEffect {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  LightState *state_{nullptr};
 | 
			
		||||
  const char *name_;
 | 
			
		||||
  std::string name_;
 | 
			
		||||
 | 
			
		||||
  /// Internal method to find this effect's index in the parent light's effect list.
 | 
			
		||||
  uint32_t get_index_in_parent_() const;
 | 
			
		||||
 
 | 
			
		||||
@@ -178,9 +178,12 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore
 | 
			
		||||
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
 | 
			
		||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
 | 
			
		||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
 | 
			
		||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
 | 
			
		||||
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
 | 
			
		||||
  // Called once from Python codegen during setup with all effects from YAML config
 | 
			
		||||
  this->effects_ = effects;
 | 
			
		||||
  this->effects_.init(effects.size());
 | 
			
		||||
  for (auto *effect : effects) {
 | 
			
		||||
    this->effects_.push_back(effect);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
 | 
			
		||||
 
 | 
			
		||||
@@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
  const FixedVector<LightEffect *> &get_effects() const;
 | 
			
		||||
 | 
			
		||||
  /// Add effects for this light state.
 | 
			
		||||
  void add_effects(const std::initializer_list<LightEffect *> &effects);
 | 
			
		||||
  void add_effects(const std::vector<LightEffect *> &effects);
 | 
			
		||||
 | 
			
		||||
  /// Get the total number of effects available for this light.
 | 
			
		||||
  size_t get_effect_count() const { return this->effects_.size(); }
 | 
			
		||||
@@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
    for (size_t i = 0; i < this->effects_.size(); i++) {
 | 
			
		||||
      if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) {
 | 
			
		||||
      if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) {
 | 
			
		||||
        return i + 1;  // Effects are 1-indexed in active_effect_index_
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -26,9 +26,9 @@ class LightTraits {
 | 
			
		||||
    this->supported_color_modes_ = ColorModeMask(modes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; }
 | 
			
		||||
  bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
 | 
			
		||||
  bool supports_color_capability(ColorCapability color_capability) const {
 | 
			
		||||
    return has_capability(this->supported_color_modes_, color_capability);
 | 
			
		||||
    return this->supported_color_modes_.has_capability(color_capability);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float get_min_mireds() const { return this->min_mireds_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -99,11 +99,7 @@ const std::string &get_use_address() {
 | 
			
		||||
  return wifi::global_wifi_component->get_use_address();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OPENTHREAD
 | 
			
		||||
  return openthread::global_openthread_component->get_use_address();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
 | 
			
		||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI)
 | 
			
		||||
  // Fallback when no network component is defined (e.g., host platform)
 | 
			
		||||
  static const std::string empty;
 | 
			
		||||
  return empty;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
@@ -278,19 +277,3 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
 | 
			
		||||
        raise EsphomeError(f"Upload failed with result: {result}")
 | 
			
		||||
 | 
			
		||||
    return handled
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
 | 
			
		||||
    address = devices[0]
 | 
			
		||||
    from .ble_logger import is_mac_address, logger_connect, logger_scan
 | 
			
		||||
 | 
			
		||||
    if devices[0] == "BLE":
 | 
			
		||||
        ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"]))
 | 
			
		||||
        if ble_device:
 | 
			
		||||
            address = ble_device.address
 | 
			
		||||
        else:
 | 
			
		||||
            return True
 | 
			
		||||
    if is_mac_address(address):
 | 
			
		||||
        asyncio.run(logger_connect(address))
 | 
			
		||||
        return True
 | 
			
		||||
    return False
 | 
			
		||||
 
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Final
 | 
			
		||||
 | 
			
		||||
from bleak import BleakClient, BleakScanner, BLEDevice
 | 
			
		||||
from bleak.exc import (
 | 
			
		||||
    BleakCharacteristicNotFoundError,
 | 
			
		||||
    BleakDBusError,
 | 
			
		||||
    BleakDeviceNotFoundError,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
 | 
			
		||||
NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
 | 
			
		||||
 | 
			
		||||
MAC_ADDRESS_PATTERN: Final = re.compile(
 | 
			
		||||
    r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_mac_address(value: str) -> bool:
 | 
			
		||||
    return MAC_ADDRESS_PATTERN.match(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def logger_scan(name: str) -> BLEDevice | None:
 | 
			
		||||
    _LOGGER.info("Scanning bluetooth for %s...", name)
 | 
			
		||||
    device = await BleakScanner.find_device_by_name(name)
 | 
			
		||||
    if not device:
 | 
			
		||||
        _LOGGER.error("%s Bluetooth LE device was not found!", name)
 | 
			
		||||
    return device
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def logger_connect(host: str) -> int | None:
 | 
			
		||||
    disconnected_event = asyncio.Event()
 | 
			
		||||
 | 
			
		||||
    def handle_disconnect(client):
 | 
			
		||||
        disconnected_event.set()
 | 
			
		||||
 | 
			
		||||
    def handle_rx(_, data: bytearray):
 | 
			
		||||
        print(data.decode("utf-8"), end="")
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Connecting %s...", host)
 | 
			
		||||
    try:
 | 
			
		||||
        async with BleakClient(host, disconnected_callback=handle_disconnect) as client:
 | 
			
		||||
            _LOGGER.info("Connected %s...", host)
 | 
			
		||||
            try:
 | 
			
		||||
                await client.start_notify(NUS_TX_CHAR_UUID, handle_rx)
 | 
			
		||||
            except BleakDBusError as e:
 | 
			
		||||
                _LOGGER.error("Bluetooth LE logger: %s", e)
 | 
			
		||||
                disconnected_event.set()
 | 
			
		||||
            await disconnected_event.wait()
 | 
			
		||||
    except BleakDeviceNotFoundError:
 | 
			
		||||
        _LOGGER.error("Device %s not found", host)
 | 
			
		||||
        return 1
 | 
			
		||||
    except BleakCharacteristicNotFoundError:
 | 
			
		||||
        _LOGGER.error("Device %s has no NUS characteristic", host)
 | 
			
		||||
        return 1
 | 
			
		||||
@@ -8,10 +8,8 @@ from esphome.components.esp32 import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
from .const import (
 | 
			
		||||
    CONF_DEVICE_TYPE,
 | 
			
		||||
@@ -110,12 +108,6 @@ _CONNECTION_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate(config: ConfigType) -> ConfigType:
 | 
			
		||||
    if CONF_USE_ADDRESS not in config:
 | 
			
		||||
        config[CONF_USE_ADDRESS] = f"{CORE.name}.local"
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _require_vfs_select(config):
 | 
			
		||||
    """Register VFS select requirement during config validation."""
 | 
			
		||||
    # OpenThread uses esp_vfs_eventfd which requires VFS select support
 | 
			
		||||
@@ -134,13 +126,11 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FORCE_DATASET): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_TLV): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(_CONNECTION_SCHEMA),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV),
 | 
			
		||||
    cv.only_with_esp_idf,
 | 
			
		||||
    only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]),
 | 
			
		||||
    _validate,
 | 
			
		||||
    _require_vfs_select,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -165,7 +155,6 @@ async def to_code(config):
 | 
			
		||||
    enable_mdns_storage()
 | 
			
		||||
 | 
			
		||||
    ot = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
 | 
			
		||||
    await cg.register_component(ot, config)
 | 
			
		||||
 | 
			
		||||
    srp = cg.new_Pvariable(config[CONF_SRP_ID])
 | 
			
		||||
 
 | 
			
		||||
@@ -252,12 +252,6 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) {
 | 
			
		||||
  ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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.
 | 
			
		||||
const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; }
 | 
			
		||||
 | 
			
		||||
void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
 | 
			
		||||
 | 
			
		||||
}  // namespace openthread
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,15 +33,11 @@ class OpenThreadComponent : public Component {
 | 
			
		||||
  void on_factory_reset(std::function<void()> callback);
 | 
			
		||||
  void defer_factory_reset_external_callback();
 | 
			
		||||
 | 
			
		||||
  const std::string &get_use_address() const;
 | 
			
		||||
  void set_use_address(const std::string &use_address);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::optional<otIp6Address> get_omr_address_(InstanceLock &lock);
 | 
			
		||||
  bool teardown_started_{false};
 | 
			
		||||
  bool teardown_complete_{false};
 | 
			
		||||
  std::function<void()> factory_reset_external_callback_;
 | 
			
		||||
  std::string use_address_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern OpenThreadComponent *global_openthread_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ void Pipsolar::loop() {
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == STATE_COMMAND_COMPLETE) {
 | 
			
		||||
    if (this->check_incoming_length_(4)) {
 | 
			
		||||
      ESP_LOGD(TAG, "response length for command OK");
 | 
			
		||||
      if (this->check_incoming_crc_()) {
 | 
			
		||||
        // crc ok
 | 
			
		||||
        if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') {
 | 
			
		||||
@@ -48,15 +49,15 @@ void Pipsolar::loop() {
 | 
			
		||||
        this->command_queue_[this->command_queue_position_] = std::string("");
 | 
			
		||||
        this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
 | 
			
		||||
      } else {
 | 
			
		||||
        // crc failed
 | 
			
		||||
        // no log message necessary, check_incoming_crc_() logs
 | 
			
		||||
        this->command_queue_[this->command_queue_position_] = std::string("");
 | 
			
		||||
        this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "command %s response length not OK: with length %zu",
 | 
			
		||||
      ESP_LOGD(TAG, "response length for command %s not OK: with length %zu",
 | 
			
		||||
               this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_);
 | 
			
		||||
      this->command_queue_[this->command_queue_position_] = std::string("");
 | 
			
		||||
      this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
 | 
			
		||||
@@ -65,10 +66,46 @@ void Pipsolar::loop() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == STATE_POLL_CHECKED) {
 | 
			
		||||
    ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command);
 | 
			
		||||
    this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier,
 | 
			
		||||
                                (const char *) this->read_buffer_);
 | 
			
		||||
    this->state_ = STATE_IDLE;
 | 
			
		||||
    switch (this->enabled_polling_commands_[this->last_polling_command_].identifier) {
 | 
			
		||||
      case POLLING_QPIRI:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QPIRI");
 | 
			
		||||
        handle_qpiri_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QPIGS:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QPIGS");
 | 
			
		||||
        handle_qpigs_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QMOD:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QMOD");
 | 
			
		||||
        handle_qmod_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QFLAG:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QFLAG");
 | 
			
		||||
        handle_qflag_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QPIWS:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QPIWS");
 | 
			
		||||
        handle_qpiws_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QT:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QT");
 | 
			
		||||
        handle_qt_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case POLLING_QMN:
 | 
			
		||||
        ESP_LOGD(TAG, "Decode QMN");
 | 
			
		||||
        handle_qmn_((const char *) this->read_buffer_);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -76,8 +113,6 @@ void Pipsolar::loop() {
 | 
			
		||||
    if (this->check_incoming_crc_()) {
 | 
			
		||||
      if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' &&
 | 
			
		||||
          this->read_buffer_[3] == 'K') {
 | 
			
		||||
        ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command);
 | 
			
		||||
        this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
 | 
			
		||||
        this->state_ = STATE_IDLE;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
@@ -86,9 +121,6 @@ void Pipsolar::loop() {
 | 
			
		||||
      this->state_ = STATE_POLL_CHECKED;
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      // crc failed
 | 
			
		||||
      // no log message necessary, check_incoming_crc_() logs
 | 
			
		||||
      this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
 | 
			
		||||
      this->state_ = STATE_IDLE;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -126,19 +158,21 @@ void Pipsolar::loop() {
 | 
			
		||||
      // command timeout
 | 
			
		||||
      const char *command = this->command_queue_[this->command_queue_position_].c_str();
 | 
			
		||||
      this->command_start_millis_ = millis();
 | 
			
		||||
      ESP_LOGD(TAG, "command %s timeout", command);
 | 
			
		||||
      ESP_LOGD(TAG, "timeout command from queue: %s", command);
 | 
			
		||||
      this->command_queue_[this->command_queue_position_] = std::string("");
 | 
			
		||||
      this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
 | 
			
		||||
      this->state_ = STATE_IDLE;
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == STATE_POLL) {
 | 
			
		||||
    if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
 | 
			
		||||
      // command timeout
 | 
			
		||||
      ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command);
 | 
			
		||||
      this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
 | 
			
		||||
      ESP_LOGD(TAG, "timeout command to poll: %s",
 | 
			
		||||
               this->enabled_polling_commands_[this->last_polling_command_].command);
 | 
			
		||||
      this->state_ = STATE_IDLE;
 | 
			
		||||
    } else {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -153,6 +187,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) {
 | 
			
		||||
uint8_t Pipsolar::check_incoming_crc_() {
 | 
			
		||||
  uint16_t crc16;
 | 
			
		||||
  crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3);
 | 
			
		||||
  ESP_LOGD(TAG, "checking crc on incoming message");
 | 
			
		||||
  if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] &&
 | 
			
		||||
      ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) {
 | 
			
		||||
    ESP_LOGD(TAG, "CRC OK");
 | 
			
		||||
@@ -218,7 +253,7 @@ bool Pipsolar::send_next_poll_() {
 | 
			
		||||
    this->write(((uint8_t) ((crc16) &0xff)));  // lowbyte
 | 
			
		||||
    // end Byte
 | 
			
		||||
    this->write(0x0D);
 | 
			
		||||
    ESP_LOGD(TAG, "Sending polling command: %s with length %d",
 | 
			
		||||
    ESP_LOGD(TAG, "Sending polling command : %s with length %d",
 | 
			
		||||
             this->enabled_polling_commands_[this->last_polling_command_].command,
 | 
			
		||||
             this->enabled_polling_commands_[this->last_polling_command_].length);
 | 
			
		||||
    return true;
 | 
			
		||||
@@ -239,38 +274,6 @@ void Pipsolar::queue_command(const std::string &command) {
 | 
			
		||||
  ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) {
 | 
			
		||||
  switch (polling_command) {
 | 
			
		||||
    case POLLING_QPIRI:
 | 
			
		||||
      handle_qpiri_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QPIGS:
 | 
			
		||||
      handle_qpigs_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QMOD:
 | 
			
		||||
      handle_qmod_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QFLAG:
 | 
			
		||||
      handle_qflag_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QPIWS:
 | 
			
		||||
      handle_qpiws_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QT:
 | 
			
		||||
      handle_qt_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    case POLLING_QMN:
 | 
			
		||||
      handle_qmn_(message);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) {
 | 
			
		||||
  // handlers are designed in a way that an empty message sets all sensors to unknown
 | 
			
		||||
  this->handle_poll_response_(polling_command, "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Pipsolar::handle_qpiri_(const char *message) {
 | 
			
		||||
  if (this->last_qpiri_) {
 | 
			
		||||
    this->last_qpiri_->publish_state(message);
 | 
			
		||||
 
 | 
			
		||||
@@ -204,9 +204,6 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  bool send_next_command_();
 | 
			
		||||
  bool send_next_poll_();
 | 
			
		||||
 | 
			
		||||
  void handle_poll_response_(ENUMPollingCommand polling_command, const char *message);
 | 
			
		||||
  void handle_poll_error_(ENUMPollingCommand polling_command);
 | 
			
		||||
  // these handlers are designed in a way that an empty message sets all sensors to unknown
 | 
			
		||||
  void handle_qpiri_(const char *message);
 | 
			
		||||
  void handle_qpigs_(const char *message);
 | 
			
		||||
  void handle_qmod_(const char *message);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,18 +4,11 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BATTERY_VOLTAGE,
 | 
			
		||||
    CONF_BUS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_BATTERY,
 | 
			
		||||
    ICON_CURRENT_AC,
 | 
			
		||||
    ICON_FLASH,
 | 
			
		||||
    ICON_GAUGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
@@ -29,10 +22,6 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 | 
			
		||||
ICON_SOLAR_POWER = "mdi:solar-power"
 | 
			
		||||
ICON_SOLAR_PANEL = "mdi:solar-panel"
 | 
			
		||||
ICON_CURRENT_DC = "mdi:current-dc"
 | 
			
		||||
 | 
			
		||||
# QPIRI sensors
 | 
			
		||||
CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage"
 | 
			
		||||
CONF_GRID_RATING_CURRENT = "grid_rating_current"
 | 
			
		||||
@@ -86,19 +75,16 @@ TYPES = {
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_GRID_RATING_CURRENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_HERTZ,
 | 
			
		||||
@@ -112,12 +98,11 @@ TYPES = {
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT_AMPS,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        device_class=DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_WATT,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
@@ -146,151 +131,124 @@ TYPES = {
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_TYPE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PARALLEL_MAX_NUM: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_MACHINE_TYPE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_TOPOLOGY: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_OUTPUT_MODE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PV_POWER_BALANCE: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_GRID_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_GRID_FREQUENCY: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_HERTZ,
 | 
			
		||||
        icon=ICON_CURRENT_AC,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_HERTZ,
 | 
			
		||||
        icon=ICON_CURRENT_AC,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT_AMPS,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        device_class=DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_WATT,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
        icon=ICON_GAUGE,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BUS_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        icon=ICON_FLASH,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        icon=ICON_BATTERY,
 | 
			
		||||
        accuracy_decimals=2,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        icon=ICON_CURRENT_DC,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        device_class=DEVICE_CLASS_BATTERY,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        icon=ICON_SOLAR_PANEL,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        icon=ICON_SOLAR_PANEL,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=2,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
        icon=ICON_CURRENT_DC,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
@@ -298,14 +256,12 @@ TYPES = {
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_EEPROM_VERSION: sensor.sensor_schema(
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
    ),
 | 
			
		||||
    CONF_PV_CHARGING_POWER: sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_WATT,
 | 
			
		||||
        icon=ICON_SOLAR_POWER,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,25 +12,6 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace remote_transmitter {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
// IDF version 5.5.1 and above is required because of a bug in
 | 
			
		||||
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
 | 
			
		||||
typedef union {  // NOLINT(modernize-use-using)
 | 
			
		||||
  struct {
 | 
			
		||||
    uint16_t duration : 15;
 | 
			
		||||
    uint16_t level : 1;
 | 
			
		||||
  };
 | 
			
		||||
  uint16_t val;
 | 
			
		||||
} rmt_symbol_half_t;
 | 
			
		||||
 | 
			
		||||
struct RemoteTransmitterComponentStore {
 | 
			
		||||
  uint32_t times{0};
 | 
			
		||||
  uint32_t index{0};
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
 | 
			
		||||
                                   public Component
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
@@ -75,14 +56,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  void configure_rmt_();
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
  RemoteTransmitterComponentStore store_{};
 | 
			
		||||
  std::vector<rmt_symbol_half_t> rmt_temp_;
 | 
			
		||||
#else
 | 
			
		||||
  std::vector<rmt_symbol_word_t> rmt_temp_;
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t current_carrier_frequency_{38000};
 | 
			
		||||
  bool initialized_{false};
 | 
			
		||||
  std::vector<rmt_symbol_word_t> rmt_temp_;
 | 
			
		||||
  bool with_dma_{false};
 | 
			
		||||
  bool eot_level_{false};
 | 
			
		||||
  rmt_channel_handle_t channel_{NULL};
 | 
			
		||||
 
 | 
			
		||||
@@ -10,46 +10,6 @@ namespace remote_transmitter {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "remote_transmitter";
 | 
			
		||||
 | 
			
		||||
// Maximum RMT symbol duration (15-bit field)
 | 
			
		||||
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free,
 | 
			
		||||
                                             rmt_symbol_word_t *symbols, bool *done, void *arg) {
 | 
			
		||||
  auto *store = static_cast<RemoteTransmitterComponentStore *>(arg);
 | 
			
		||||
  const auto *encoded = static_cast<const rmt_symbol_half_t *>(data);
 | 
			
		||||
  size_t length = size / sizeof(rmt_symbol_half_t);
 | 
			
		||||
  size_t count = 0;
 | 
			
		||||
 | 
			
		||||
  // copy symbols
 | 
			
		||||
  for (size_t i = 0; i < free; i++) {
 | 
			
		||||
    uint16_t sym_0 = encoded[store->index++].val;
 | 
			
		||||
    if (store->index >= length) {
 | 
			
		||||
      store->index = 0;
 | 
			
		||||
      store->times--;
 | 
			
		||||
      if (store->times == 0) {
 | 
			
		||||
        *done = true;
 | 
			
		||||
        symbols[count++].val = sym_0;
 | 
			
		||||
        return count;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    uint16_t sym_1 = encoded[store->index++].val;
 | 
			
		||||
    if (store->index >= length) {
 | 
			
		||||
      store->index = 0;
 | 
			
		||||
      store->times--;
 | 
			
		||||
      if (store->times == 0) {
 | 
			
		||||
        *done = true;
 | 
			
		||||
        symbols[count++].val = sym_0 | (sym_1 << 16);
 | 
			
		||||
        return count;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    symbols[count++].val = sym_0 | (sym_1 << 16);
 | 
			
		||||
  }
 | 
			
		||||
  *done = false;
 | 
			
		||||
  return count;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::setup() {
 | 
			
		||||
  this->inverted_ = this->pin_->is_inverted();
 | 
			
		||||
  this->configure_rmt_();
 | 
			
		||||
@@ -74,17 +34,6 @@ void RemoteTransmitterComponent::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RemoteTransmitterComponent::digital_write(bool value) {
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
  rmt_symbol_half_t symbol = {
 | 
			
		||||
      .duration = 1,
 | 
			
		||||
      .level = value,
 | 
			
		||||
  };
 | 
			
		||||
  rmt_transmit_config_t config;
 | 
			
		||||
  memset(&config, 0, sizeof(config));
 | 
			
		||||
  config.flags.eot_level = value;
 | 
			
		||||
  this->store_.times = 1;
 | 
			
		||||
  this->store_.index = 0;
 | 
			
		||||
#else
 | 
			
		||||
  rmt_symbol_word_t symbol = {
 | 
			
		||||
      .duration0 = 1,
 | 
			
		||||
      .level0 = value,
 | 
			
		||||
@@ -93,8 +42,8 @@ void RemoteTransmitterComponent::digital_write(bool value) {
 | 
			
		||||
  };
 | 
			
		||||
  rmt_transmit_config_t config;
 | 
			
		||||
  memset(&config, 0, sizeof(config));
 | 
			
		||||
  config.loop_count = 0;
 | 
			
		||||
  config.flags.eot_level = value;
 | 
			
		||||
#endif
 | 
			
		||||
  esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config);
 | 
			
		||||
  if (error != ESP_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
 | 
			
		||||
@@ -141,20 +90,6 @@ void RemoteTransmitterComponent::configure_rmt_() {
 | 
			
		||||
      gpio_pullup_dis(gpio_num_t(this->pin_->get_pin()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
    rmt_simple_encoder_config_t encoder;
 | 
			
		||||
    memset(&encoder, 0, sizeof(encoder));
 | 
			
		||||
    encoder.callback = encoder_callback;
 | 
			
		||||
    encoder.arg = &this->store_;
 | 
			
		||||
    encoder.min_chunk_size = 1;
 | 
			
		||||
    error = rmt_new_simple_encoder(&encoder, &this->encoder_);
 | 
			
		||||
    if (error != ESP_OK) {
 | 
			
		||||
      this->error_code_ = error;
 | 
			
		||||
      this->error_string_ = "in rmt_new_simple_encoder";
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
#else
 | 
			
		||||
    rmt_copy_encoder_config_t encoder;
 | 
			
		||||
    memset(&encoder, 0, sizeof(encoder));
 | 
			
		||||
    error = rmt_new_copy_encoder(&encoder, &this->encoder_);
 | 
			
		||||
@@ -164,7 +99,6 @@ void RemoteTransmitterComponent::configure_rmt_() {
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    error = rmt_enable(this->channel_);
 | 
			
		||||
    if (error != ESP_OK) {
 | 
			
		||||
@@ -196,79 +130,6 @@ void RemoteTransmitterComponent::configure_rmt_() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
 | 
			
		||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
 | 
			
		||||
    this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
 | 
			
		||||
    this->configure_rmt_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->rmt_temp_.clear();
 | 
			
		||||
  this->rmt_temp_.reserve(this->temp_.get_data().size() + 1);
 | 
			
		||||
 | 
			
		||||
  // encode any delay at the start of the buffer to simplify the encoder callback
 | 
			
		||||
  // this will be skipped the first time around
 | 
			
		||||
  send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
 | 
			
		||||
  while (send_wait > 0) {
 | 
			
		||||
    int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
 | 
			
		||||
    this->rmt_temp_.push_back({
 | 
			
		||||
        .duration = static_cast<uint16_t>(duration),
 | 
			
		||||
        .level = static_cast<uint16_t>(this->eot_level_),
 | 
			
		||||
    });
 | 
			
		||||
    send_wait -= duration;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // encode data
 | 
			
		||||
  size_t offset = this->rmt_temp_.size();
 | 
			
		||||
  for (int32_t value : this->temp_.get_data()) {
 | 
			
		||||
    bool level = value >= 0;
 | 
			
		||||
    if (!level) {
 | 
			
		||||
      value = -value;
 | 
			
		||||
    }
 | 
			
		||||
    value = this->from_microseconds_(static_cast<uint32_t>(value));
 | 
			
		||||
    while (value > 0) {
 | 
			
		||||
      int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
 | 
			
		||||
      this->rmt_temp_.push_back({
 | 
			
		||||
          .duration = static_cast<uint16_t>(duration),
 | 
			
		||||
          .level = static_cast<uint16_t>(level ^ this->inverted_),
 | 
			
		||||
      });
 | 
			
		||||
      value -= duration;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
 | 
			
		||||
    ESP_LOGE(TAG, "Empty data");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->transmit_trigger_->trigger();
 | 
			
		||||
 | 
			
		||||
  rmt_transmit_config_t config;
 | 
			
		||||
  memset(&config, 0, sizeof(config));
 | 
			
		||||
  config.flags.eot_level = this->eot_level_;
 | 
			
		||||
  this->store_.times = send_times;
 | 
			
		||||
  this->store_.index = offset;
 | 
			
		||||
  esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
 | 
			
		||||
                                 this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config);
 | 
			
		||||
  if (error != ESP_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
  } else {
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  }
 | 
			
		||||
  error = rmt_tx_wait_all_done(this->channel_, -1);
 | 
			
		||||
  if (error != ESP_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->complete_trigger_->trigger();
 | 
			
		||||
}
 | 
			
		||||
#else
 | 
			
		||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
 | 
			
		||||
  if (this->is_failed())
 | 
			
		||||
    return;
 | 
			
		||||
@@ -290,7 +151,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
 | 
			
		||||
    val = this->from_microseconds_(static_cast<uint32_t>(val));
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
      int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX));
 | 
			
		||||
      int32_t item = std::min(val, int32_t(32767));
 | 
			
		||||
      val -= item;
 | 
			
		||||
 | 
			
		||||
      if (rmt_i % 2 == 0) {
 | 
			
		||||
@@ -319,6 +180,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
 | 
			
		||||
  for (uint32_t i = 0; i < send_times; i++) {
 | 
			
		||||
    rmt_transmit_config_t config;
 | 
			
		||||
    memset(&config, 0, sizeof(config));
 | 
			
		||||
    config.loop_count = 0;
 | 
			
		||||
    config.flags.eot_level = this->eot_level_;
 | 
			
		||||
    esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
 | 
			
		||||
                                   this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config);
 | 
			
		||||
@@ -338,7 +200,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
 | 
			
		||||
  }
 | 
			
		||||
  this->complete_trigger_->trigger();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace remote_transmitter
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -878,9 +878,7 @@ async def setup_sensor_core_(var, config):
 | 
			
		||||
        cg.add(var.set_unit_of_measurement(unit_of_measurement))
 | 
			
		||||
    if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
 | 
			
		||||
        cg.add(var.set_accuracy_decimals(accuracy_decimals))
 | 
			
		||||
    # Only set force_update if True (default is False)
 | 
			
		||||
    if config[CONF_FORCE_UPDATE]:
 | 
			
		||||
        cg.add(var.set_force_update(True))
 | 
			
		||||
    cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
 | 
			
		||||
    if config.get(CONF_FILTERS):  # must exist and not be empty
 | 
			
		||||
        filters = await build_filters(config[CONF_FILTERS])
 | 
			
		||||
        cg.add(var.set_filters(filters))
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ void SNTPComponent::setup() {
 | 
			
		||||
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
 | 
			
		||||
  size_t i = 0;
 | 
			
		||||
  for (auto &server : this->servers_) {
 | 
			
		||||
    esp_sntp_setservername(i++, server);
 | 
			
		||||
    esp_sntp_setservername(i++, server.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  esp_sntp_set_sync_interval(this->get_update_interval());
 | 
			
		||||
  esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) {
 | 
			
		||||
@@ -42,7 +42,7 @@ void SNTPComponent::setup() {
 | 
			
		||||
 | 
			
		||||
  size_t i = 0;
 | 
			
		||||
  for (auto &server : this->servers_) {
 | 
			
		||||
    sntp_setservername(i++, server);
 | 
			
		||||
    sntp_setservername(i++, server.c_str());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP8266)
 | 
			
		||||
@@ -59,7 +59,7 @@ void SNTPComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "SNTP Time:");
 | 
			
		||||
  size_t i = 0;
 | 
			
		||||
  for (auto &server : this->servers_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server.c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void SNTPComponent::update() {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,10 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/time/real_time_clock.h"
 | 
			
		||||
#include <array>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sntp {
 | 
			
		||||
 | 
			
		||||
// Server count is calculated at compile time by Python codegen
 | 
			
		||||
// SNTP_SERVER_COUNT will always be defined
 | 
			
		||||
 | 
			
		||||
/// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol.
 | 
			
		||||
///
 | 
			
		||||
/// \note
 | 
			
		||||
@@ -18,7 +14,10 @@ namespace sntp {
 | 
			
		||||
/// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
 | 
			
		||||
class SNTPComponent : public time::RealTimeClock {
 | 
			
		||||
 public:
 | 
			
		||||
  SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {}
 | 
			
		||||
  SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {}
 | 
			
		||||
 | 
			
		||||
  // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would
 | 
			
		||||
  // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents.
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -30,10 +29,7 @@ class SNTPComponent : public time::RealTimeClock {
 | 
			
		||||
  void time_synced();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Store const char pointers to string literals
 | 
			
		||||
  // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each)
 | 
			
		||||
  // Other platforms: strings in flash
 | 
			
		||||
  std::array<const char *, SNTP_SERVER_COUNT> servers_;
 | 
			
		||||
  std::vector<std::string> servers_;
 | 
			
		||||
  bool has_time_{false};
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,11 +43,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    servers = config[CONF_SERVERS]
 | 
			
		||||
 | 
			
		||||
    # Define server count at compile time
 | 
			
		||||
    cg.add_define("SNTP_SERVER_COUNT", len(servers))
 | 
			
		||||
 | 
			
		||||
    # Pass string literals to constructor - stored in flash/rodata by compiler
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], servers)
 | 
			
		||||
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -82,12 +82,6 @@ struct TransferStatus {
 | 
			
		||||
 | 
			
		||||
using transfer_cb_t = std::function<void(const TransferStatus &)>;
 | 
			
		||||
 | 
			
		||||
enum TransferResult : uint8_t {
 | 
			
		||||
  TRANSFER_OK = 0,
 | 
			
		||||
  TRANSFER_ERROR_NO_SLOTS,
 | 
			
		||||
  TRANSFER_ERROR_SUBMIT_FAILED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class USBClient;
 | 
			
		||||
 | 
			
		||||
// struct used to capture all data needed for a transfer
 | 
			
		||||
@@ -140,7 +134,7 @@ class USBClient : public Component {
 | 
			
		||||
  void on_opened(uint8_t addr);
 | 
			
		||||
  void on_removed(usb_device_handle_t handle);
 | 
			
		||||
  void control_transfer_callback(const usb_transfer_t *xfer) const;
 | 
			
		||||
  TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
 | 
			
		||||
  void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
 | 
			
		||||
  void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void release_trq(TransferRequest *trq);
 | 
			
		||||
 
 | 
			
		||||
@@ -334,7 +334,7 @@ static void control_callback(const usb_transfer_t *xfer) {
 | 
			
		||||
// This multi-threaded access is intentional for performance - USB task can
 | 
			
		||||
// immediately restart transfers without waiting for main loop scheduling.
 | 
			
		||||
TransferRequest *USBClient::get_trq_() {
 | 
			
		||||
  trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
 | 
			
		||||
  trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
 | 
			
		||||
 | 
			
		||||
  // Find first available slot (bit = 0) and try to claim it atomically
 | 
			
		||||
  // We use a while loop to allow retrying the same slot after CAS failure
 | 
			
		||||
@@ -443,15 +443,14 @@ static void transfer_callback(usb_transfer_t *xfer) {
 | 
			
		||||
 * @param ep_address The endpoint address.
 | 
			
		||||
 * @param callback The callback function to be called when the transfer is complete.
 | 
			
		||||
 * @param length The length of the data to be transferred.
 | 
			
		||||
 * @return TransferResult indicating success or specific failure reason
 | 
			
		||||
 *
 | 
			
		||||
 * @throws None.
 | 
			
		||||
 */
 | 
			
		||||
TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
 | 
			
		||||
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
 | 
			
		||||
  auto *trq = this->get_trq_();
 | 
			
		||||
  if (trq == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Too many requests queued");
 | 
			
		||||
    return TRANSFER_ERROR_NO_SLOTS;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  trq->callback = callback;
 | 
			
		||||
  trq->transfer->callback = transfer_callback;
 | 
			
		||||
@@ -461,9 +460,7 @@ TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &c
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
 | 
			
		||||
    this->release_trq(trq);
 | 
			
		||||
    return TRANSFER_ERROR_SUBMIT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  return TRANSFER_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -169,98 +169,6 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
 | 
			
		||||
  this->parent_->start_input(this);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
void USBUartComponent::reset_input_state_(USBUartChannel *channel) {
 | 
			
		||||
  channel->input_retry_count_.store(0);
 | 
			
		||||
  channel->input_started_.store(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::restart_input_(USBUartChannel *channel) {
 | 
			
		||||
  // Atomically verify it's still started (true) and keep it started
 | 
			
		||||
  // This prevents the race window of toggling true->false->true
 | 
			
		||||
  bool expected = true;
 | 
			
		||||
  if (channel->input_started_.compare_exchange_strong(expected, true)) {
 | 
			
		||||
    // Still started - do the actual restart work without toggling the flag
 | 
			
		||||
    this->do_start_input_(channel);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) {
 | 
			
		||||
  // CALLBACK CONTEXT: This function is executed in USB task via transfer_callback
 | 
			
		||||
  ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
 | 
			
		||||
 | 
			
		||||
  if (!status.success) {
 | 
			
		||||
    ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
 | 
			
		||||
    // Transfer failed, slot already released
 | 
			
		||||
    // Reset state so normal operations can restart later
 | 
			
		||||
    this->reset_input_state_(channel);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!channel->dummy_receiver_ && status.data_len > 0) {
 | 
			
		||||
    // Allocate a chunk from the pool
 | 
			
		||||
    UsbDataChunk *chunk = this->chunk_pool_.allocate();
 | 
			
		||||
    if (chunk == nullptr) {
 | 
			
		||||
      // No chunks available - queue is full, data dropped, slot already released
 | 
			
		||||
      this->usb_data_queue_.increment_dropped_count();
 | 
			
		||||
      // Reset state so normal operations can restart later
 | 
			
		||||
      this->reset_input_state_(channel);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Copy data to chunk (this is fast, happens in USB task)
 | 
			
		||||
    memcpy(chunk->data, status.data, status.data_len);
 | 
			
		||||
    chunk->length = status.data_len;
 | 
			
		||||
    chunk->channel = channel;
 | 
			
		||||
 | 
			
		||||
    // Push to lock-free queue for main loop processing
 | 
			
		||||
    // Push always succeeds because pool size == queue size
 | 
			
		||||
    this->usb_data_queue_.push(chunk);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // On success, reset retry count and restart input immediately from USB task for performance
 | 
			
		||||
  // The lock-free queue will handle backpressure
 | 
			
		||||
  channel->input_retry_count_.store(0);
 | 
			
		||||
  channel->input_started_.store(false);
 | 
			
		||||
  this->start_input(channel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::do_start_input_(USBUartChannel *channel) {
 | 
			
		||||
  // This function does the actual work of starting input
 | 
			
		||||
  // Caller must ensure input_started_ is already set to true
 | 
			
		||||
  const auto *ep = channel->cdc_dev_.in_ep;
 | 
			
		||||
 | 
			
		||||
  // input_started_ already set to true by caller
 | 
			
		||||
  auto result = this->transfer_in(
 | 
			
		||||
      ep->bEndpointAddress,
 | 
			
		||||
      [this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); },
 | 
			
		||||
      ep->wMaxPacketSize);
 | 
			
		||||
 | 
			
		||||
  if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) {
 | 
			
		||||
    // No slots available - defer retry to main loop
 | 
			
		||||
    this->defer_input_retry_(channel);
 | 
			
		||||
  } else if (result != usb_host::TRANSFER_OK) {
 | 
			
		||||
    // Other error (submit failed) - don't retry, just reset state
 | 
			
		||||
    // Error already logged by transfer_in()
 | 
			
		||||
    this->reset_input_state_(channel);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::defer_input_retry_(USBUartChannel *channel) {
 | 
			
		||||
  static constexpr uint8_t MAX_INPUT_RETRIES = 10;
 | 
			
		||||
 | 
			
		||||
  // Atomically increment and get the NEW value (previous + 1)
 | 
			
		||||
  uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1;
 | 
			
		||||
  if (new_retry_count > MAX_INPUT_RETRIES) {
 | 
			
		||||
    ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_);
 | 
			
		||||
    this->reset_input_state_(channel);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Keep input_started_ as true during defer to prevent multiple retries from queueing
 | 
			
		||||
  // The deferred lambda will atomically restart
 | 
			
		||||
  this->defer([this, channel] { this->restart_input_(channel); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::setup() { USBClient::setup(); }
 | 
			
		||||
void USBUartComponent::loop() {
 | 
			
		||||
  USBClient::loop();
 | 
			
		||||
@@ -306,14 +214,8 @@ void USBUartComponent::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBUartComponent::start_input(USBUartChannel *channel) {
 | 
			
		||||
  if (!channel->initialised_.load())
 | 
			
		||||
  if (!channel->initialised_.load() || channel->input_started_.load())
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  // Atomically check if not started and set to started in one operation
 | 
			
		||||
  bool expected = false;
 | 
			
		||||
  if (!channel->input_started_.compare_exchange_strong(expected, true))
 | 
			
		||||
    return;  // Already started - prevents duplicate transfers from concurrent threads
 | 
			
		||||
 | 
			
		||||
  // THREAD CONTEXT: Called from both USB task and main loop threads
 | 
			
		||||
  // - USB task: Immediate restart after successful transfer for continuous data flow
 | 
			
		||||
  // - Main loop: Controlled restart after consuming data (backpressure mechanism)
 | 
			
		||||
@@ -324,9 +226,45 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
 | 
			
		||||
  //
 | 
			
		||||
  // The underlying transfer_in() uses lock-free atomic allocation from the
 | 
			
		||||
  // TransferRequest pool, making this multi-threaded access safe
 | 
			
		||||
  const auto *ep = channel->cdc_dev_.in_ep;
 | 
			
		||||
  // CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
 | 
			
		||||
  auto callback = [this, channel](const usb_host::TransferStatus &status) {
 | 
			
		||||
    ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
 | 
			
		||||
    if (!status.success) {
 | 
			
		||||
      ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
 | 
			
		||||
      // On failure, don't restart - let next read_array() trigger it
 | 
			
		||||
      channel->input_started_.store(false);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  // Do the actual work (input_started_ already set to true by CAS above)
 | 
			
		||||
  this->do_start_input_(channel);
 | 
			
		||||
    if (!channel->dummy_receiver_ && status.data_len > 0) {
 | 
			
		||||
      // Allocate a chunk from the pool
 | 
			
		||||
      UsbDataChunk *chunk = this->chunk_pool_.allocate();
 | 
			
		||||
      if (chunk == nullptr) {
 | 
			
		||||
        // No chunks available - queue is full or we're out of memory
 | 
			
		||||
        this->usb_data_queue_.increment_dropped_count();
 | 
			
		||||
        // Mark input as not started so we can retry
 | 
			
		||||
        channel->input_started_.store(false);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Copy data to chunk (this is fast, happens in USB task)
 | 
			
		||||
      memcpy(chunk->data, status.data, status.data_len);
 | 
			
		||||
      chunk->length = status.data_len;
 | 
			
		||||
      chunk->channel = channel;
 | 
			
		||||
 | 
			
		||||
      // Push to lock-free queue for main loop processing
 | 
			
		||||
      // Push always succeeds because pool size == queue size
 | 
			
		||||
      this->usb_data_queue_.push(chunk);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // On success, restart input immediately from USB task for performance
 | 
			
		||||
    // The lock-free queue will handle backpressure
 | 
			
		||||
    channel->input_started_.store(false);
 | 
			
		||||
    this->start_input(channel);
 | 
			
		||||
  };
 | 
			
		||||
  channel->input_started_.store(true);
 | 
			
		||||
  this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::start_output(USBUartChannel *channel) {
 | 
			
		||||
@@ -432,7 +370,7 @@ void USBUartTypeCdcAcm::enable_channels() {
 | 
			
		||||
  for (auto *channel : this->channels_) {
 | 
			
		||||
    if (!channel->initialised_.load())
 | 
			
		||||
      continue;
 | 
			
		||||
    this->reset_input_state_(channel);
 | 
			
		||||
    channel->input_started_.store(false);
 | 
			
		||||
    channel->output_started_.store(false);
 | 
			
		||||
    this->start_input(channel);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -111,11 +111,10 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
 | 
			
		||||
  CdcEps cdc_dev_{};
 | 
			
		||||
  // Enum (likely 4 bytes)
 | 
			
		||||
  UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
 | 
			
		||||
  // Group atomics together
 | 
			
		||||
  // Group atomics together (each 1 byte)
 | 
			
		||||
  std::atomic<bool> input_started_{true};
 | 
			
		||||
  std::atomic<bool> output_started_{true};
 | 
			
		||||
  std::atomic<bool> initialised_{false};
 | 
			
		||||
  std::atomic<uint8_t> input_retry_count_{0};
 | 
			
		||||
  // Group regular bytes together to minimize padding
 | 
			
		||||
  const uint8_t index_;
 | 
			
		||||
  bool debug_{};
 | 
			
		||||
@@ -141,11 +140,6 @@ class USBUartComponent : public usb_host::USBClient {
 | 
			
		||||
  EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void defer_input_retry_(USBUartChannel *channel);
 | 
			
		||||
  void reset_input_state_(USBUartChannel *channel);
 | 
			
		||||
  void restart_input_(USBUartChannel *channel);
 | 
			
		||||
  void do_start_input_(USBUartChannel *channel);
 | 
			
		||||
  void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status);
 | 
			
		||||
  std::vector<USBUartChannel *> channels_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -213,15 +213,11 @@ def _validate(config):
 | 
			
		||||
        if CONF_EAP in config:
 | 
			
		||||
            network[CONF_EAP] = config.pop(CONF_EAP)
 | 
			
		||||
        if CONF_NETWORKS in config:
 | 
			
		||||
            # In testing mode, merged component tests may have both ssid and networks
 | 
			
		||||
            # Just use the networks list and ignore the single ssid
 | 
			
		||||
            if not CORE.testing_mode:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    "You cannot use the 'ssid:' option together with 'networks:'. Please "
 | 
			
		||||
                    "copy your network into the 'networks:' key"
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "You cannot use the 'ssid:' option together with 'networks:'. Please "
 | 
			
		||||
                "copy your network into the 'networks:' key"
 | 
			
		||||
            )
 | 
			
		||||
        config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network)
 | 
			
		||||
 | 
			
		||||
    if (CONF_NETWORKS not in config) and (CONF_AP not in config):
 | 
			
		||||
        config = config.copy()
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ const int DEFAULT_BLANK_TIME = 1000;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "wled_light_effect";
 | 
			
		||||
 | 
			
		||||
WLEDLightEffect::WLEDLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
void WLEDLightEffect::start() {
 | 
			
		||||
  AddressableLightEffect::start();
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace wled {
 | 
			
		||||
 | 
			
		||||
class WLEDLightEffect : public light::AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  WLEDLightEffect(const char *name);
 | 
			
		||||
  WLEDLightEffect(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -234,9 +234,6 @@ def copy_files():
 | 
			
		||||
    "url": "https://esphome.io/",
 | 
			
		||||
    "vendor": "esphome",
 | 
			
		||||
    "build": {
 | 
			
		||||
        "bsp": {
 | 
			
		||||
            "name": "adafruit"
 | 
			
		||||
        },
 | 
			
		||||
        "softdevice": {
 | 
			
		||||
            "sd_fwid": "0x00B6"
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -636,9 +636,11 @@ class EsphomeCore:
 | 
			
		||||
        if self.config is None:
 | 
			
		||||
            raise ValueError("Config has not been loaded yet")
 | 
			
		||||
 | 
			
		||||
        for network_type in (CONF_WIFI, CONF_ETHERNET, CONF_OPENTHREAD):
 | 
			
		||||
            if network_type in self.config:
 | 
			
		||||
                return self.config[network_type][CONF_USE_ADDRESS]
 | 
			
		||||
        if CONF_WIFI in self.config:
 | 
			
		||||
            return self.config[CONF_WIFI][CONF_USE_ADDRESS]
 | 
			
		||||
 | 
			
		||||
        if CONF_ETHERNET in self.config:
 | 
			
		||||
            return self.config[CONF_ETHERNET][CONF_USE_ADDRESS]
 | 
			
		||||
 | 
			
		||||
        if CONF_OPENTHREAD in self.config:
 | 
			
		||||
            return f"{self.name}.local"
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,6 @@
 | 
			
		||||
#define USE_MDNS_STORE_SERVICES
 | 
			
		||||
#define MDNS_SERVICE_COUNT 3
 | 
			
		||||
#define MDNS_DYNAMIC_TXT_COUNT 3
 | 
			
		||||
#define SNTP_SERVER_COUNT 3
 | 
			
		||||
#define USE_MEDIA_PLAYER
 | 
			
		||||
#define USE_NEXTION_TFT_UPLOAD
 | 
			
		||||
#define USE_NUMBER
 | 
			
		||||
 
 | 
			
		||||
@@ -105,9 +105,7 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
 | 
			
		||||
        config[CONF_NAME],
 | 
			
		||||
        platform,
 | 
			
		||||
    )
 | 
			
		||||
    # Only set disabled_by_default if True (default is False)
 | 
			
		||||
    if config[CONF_DISABLED_BY_DEFAULT]:
 | 
			
		||||
        add(var.set_disabled_by_default(True))
 | 
			
		||||
    add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
 | 
			
		||||
    if CONF_INTERNAL in config:
 | 
			
		||||
        add(var.set_internal(config[CONF_INTERNAL]))
 | 
			
		||||
    if CONF_ICON in config:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,171 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <initializer_list>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#include <type_traits>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
/// Default bit mapping policy for contiguous enums starting at 0
 | 
			
		||||
/// Provides 1:1 mapping where enum value equals bit position
 | 
			
		||||
template<typename ValueType, int MaxBits> struct DefaultBitPolicy {
 | 
			
		||||
  // Automatic bitmask type selection based on MaxBits
 | 
			
		||||
  // ≤8 bits: uint8_t, ≤16 bits: uint16_t, otherwise: uint32_t
 | 
			
		||||
  using mask_t = typename std::conditional<(MaxBits <= 8), uint8_t,
 | 
			
		||||
                                           typename std::conditional<(MaxBits <= 16), uint16_t, uint32_t>::type>::type;
 | 
			
		||||
 | 
			
		||||
  static constexpr int MAX_BITS = MaxBits;
 | 
			
		||||
 | 
			
		||||
  static constexpr unsigned to_bit(ValueType value) { return static_cast<unsigned>(value); }
 | 
			
		||||
 | 
			
		||||
  static constexpr ValueType from_bit(unsigned bit) { return static_cast<ValueType>(bit); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Generic bitmask for storing a finite set of discrete values efficiently.
 | 
			
		||||
/// Replaces std::set<ValueType> to eliminate red-black tree overhead (~586 bytes per instantiation).
 | 
			
		||||
///
 | 
			
		||||
/// Template parameters:
 | 
			
		||||
///   ValueType: The type to store (typically enum, but can be any discrete bounded type)
 | 
			
		||||
///   BitPolicy: Policy class defining bit mapping and mask type (defaults to DefaultBitPolicy)
 | 
			
		||||
///
 | 
			
		||||
/// BitPolicy requirements:
 | 
			
		||||
///   - using mask_t = <uint8_t|uint16_t|uint32_t>  // Bitmask storage type
 | 
			
		||||
///   - static constexpr int MAX_BITS               // Maximum number of bits
 | 
			
		||||
///   - static constexpr unsigned to_bit(ValueType) // Convert value to bit position
 | 
			
		||||
///   - static constexpr ValueType from_bit(unsigned) // Convert bit position to value
 | 
			
		||||
///
 | 
			
		||||
/// Example usage (1:1 mapping - climate enums):
 | 
			
		||||
///   // For contiguous enums starting at 0, use DefaultBitPolicy
 | 
			
		||||
///   using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
 | 
			
		||||
///   ClimateModeMask modes({CLIMATE_MODE_HEAT, CLIMATE_MODE_COOL});
 | 
			
		||||
///   if (modes.count(CLIMATE_MODE_HEAT)) { ... }
 | 
			
		||||
///   for (auto mode : modes) { ... }
 | 
			
		||||
///
 | 
			
		||||
/// Example usage (custom mapping - ColorMode):
 | 
			
		||||
///   // For custom mappings, define a custom BitPolicy
 | 
			
		||||
///   // See esphome/components/light/color_mode.h for complete example
 | 
			
		||||
///
 | 
			
		||||
/// Design notes:
 | 
			
		||||
///   - Policy-based design allows custom bit mappings without template specialization
 | 
			
		||||
///   - Iterator converts bit positions to actual values during traversal
 | 
			
		||||
///   - All operations are constexpr-compatible for compile-time initialization
 | 
			
		||||
///   - Drop-in replacement for std::set<ValueType> with simpler API
 | 
			
		||||
///
 | 
			
		||||
template<typename ValueType, typename BitPolicy = DefaultBitPolicy<ValueType, 16>> class FiniteSetMask {
 | 
			
		||||
 public:
 | 
			
		||||
  using bitmask_t = typename BitPolicy::mask_t;
 | 
			
		||||
 | 
			
		||||
  constexpr FiniteSetMask() = default;
 | 
			
		||||
 | 
			
		||||
  /// Construct from initializer list: {VALUE1, VALUE2, ...}
 | 
			
		||||
  constexpr FiniteSetMask(std::initializer_list<ValueType> values) {
 | 
			
		||||
    for (auto value : values) {
 | 
			
		||||
      this->insert(value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Add a single value to the set (std::set compatibility)
 | 
			
		||||
  constexpr void insert(ValueType value) { this->mask_ |= (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
 | 
			
		||||
 | 
			
		||||
  /// Add multiple values from initializer list
 | 
			
		||||
  constexpr void insert(std::initializer_list<ValueType> values) {
 | 
			
		||||
    for (auto value : values) {
 | 
			
		||||
      this->insert(value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Remove a value from the set (std::set compatibility)
 | 
			
		||||
  constexpr void erase(ValueType value) { this->mask_ &= ~(static_cast<bitmask_t>(1) << BitPolicy::to_bit(value)); }
 | 
			
		||||
 | 
			
		||||
  /// Clear all values from the set
 | 
			
		||||
  constexpr void clear() { this->mask_ = 0; }
 | 
			
		||||
 | 
			
		||||
  /// Check if the set contains a specific value (std::set compatibility)
 | 
			
		||||
  /// Returns 1 if present, 0 if not (same as std::set for unique elements)
 | 
			
		||||
  constexpr size_t count(ValueType value) const {
 | 
			
		||||
    return (this->mask_ & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0 ? 1 : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Count the number of values in the set
 | 
			
		||||
  constexpr size_t size() const {
 | 
			
		||||
    // Brian Kernighan's algorithm - efficient for sparse bitmasks
 | 
			
		||||
    // Typical case: 2-4 modes out of 10 possible
 | 
			
		||||
    bitmask_t n = this->mask_;
 | 
			
		||||
    size_t count = 0;
 | 
			
		||||
    while (n) {
 | 
			
		||||
      n &= n - 1;  // Clear the least significant set bit
 | 
			
		||||
      count++;
 | 
			
		||||
    }
 | 
			
		||||
    return count;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Check if the set is empty
 | 
			
		||||
  constexpr bool empty() const { return this->mask_ == 0; }
 | 
			
		||||
 | 
			
		||||
  /// Iterator support for range-based for loops and API encoding
 | 
			
		||||
  /// Iterates over set bits and converts bit positions to values
 | 
			
		||||
  /// Optimization: removes bits from mask as we iterate
 | 
			
		||||
  class Iterator {
 | 
			
		||||
   public:
 | 
			
		||||
    using iterator_category = std::forward_iterator_tag;
 | 
			
		||||
    using value_type = ValueType;
 | 
			
		||||
    using difference_type = std::ptrdiff_t;
 | 
			
		||||
    using pointer = const ValueType *;
 | 
			
		||||
    using reference = ValueType;
 | 
			
		||||
 | 
			
		||||
    constexpr explicit Iterator(bitmask_t mask) : mask_(mask) {}
 | 
			
		||||
 | 
			
		||||
    constexpr ValueType operator*() const {
 | 
			
		||||
      // Return value for the first set bit
 | 
			
		||||
      return BitPolicy::from_bit(find_next_set_bit(mask_, 0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr Iterator &operator++() {
 | 
			
		||||
      // Clear the lowest set bit (Brian Kernighan's algorithm)
 | 
			
		||||
      mask_ &= mask_ - 1;
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator==(const Iterator &other) const { return mask_ == other.mask_; }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
 | 
			
		||||
 | 
			
		||||
   private:
 | 
			
		||||
    bitmask_t mask_;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constexpr Iterator begin() const { return Iterator(mask_); }
 | 
			
		||||
  constexpr Iterator end() const { return Iterator(0); }
 | 
			
		||||
 | 
			
		||||
  /// Get the raw bitmask value for optimized operations
 | 
			
		||||
  constexpr bitmask_t get_mask() const { return this->mask_; }
 | 
			
		||||
 | 
			
		||||
  /// Check if a specific value is present in a raw bitmask
 | 
			
		||||
  /// Useful for checking intersection results without creating temporary objects
 | 
			
		||||
  static constexpr bool mask_contains(bitmask_t mask, ValueType value) {
 | 
			
		||||
    return (mask & (static_cast<bitmask_t>(1) << BitPolicy::to_bit(value))) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Get the first value from a raw bitmask
 | 
			
		||||
  /// Used for optimizing intersection logic (e.g., "pick first suitable mode")
 | 
			
		||||
  static constexpr ValueType first_value_from_mask(bitmask_t mask) {
 | 
			
		||||
    return BitPolicy::from_bit(find_next_set_bit(mask, 0));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Find the next set bit in a bitmask starting from a given position
 | 
			
		||||
  /// Returns the bit position, or MAX_BITS if no more bits are set
 | 
			
		||||
  static constexpr int find_next_set_bit(bitmask_t mask, int start_bit) {
 | 
			
		||||
    int bit = start_bit;
 | 
			
		||||
    while (bit < BitPolicy::MAX_BITS && !(mask & (static_cast<bitmask_t>(1) << bit))) {
 | 
			
		||||
      ++bit;
 | 
			
		||||
    }
 | 
			
		||||
    return bit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bitmask_t mask_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -224,37 +224,36 @@ def resolve_ip_address(
 | 
			
		||||
        return res
 | 
			
		||||
 | 
			
		||||
    # Process hosts
 | 
			
		||||
 | 
			
		||||
    cached_addresses: list[str] = []
 | 
			
		||||
    uncached_hosts: list[str] = []
 | 
			
		||||
    has_cache = address_cache is not None
 | 
			
		||||
 | 
			
		||||
    for h in hosts:
 | 
			
		||||
        if is_ip_address(h):
 | 
			
		||||
            _add_ip_addresses_to_addrinfo([h], port, res)
 | 
			
		||||
            if has_cache:
 | 
			
		||||
                # If we have a cache, treat IPs as cached
 | 
			
		||||
                cached_addresses.append(h)
 | 
			
		||||
            else:
 | 
			
		||||
                # If no cache, pass IPs through to resolver with hostnames
 | 
			
		||||
                uncached_hosts.append(h)
 | 
			
		||||
        elif address_cache and (cached := address_cache.get_addresses(h)):
 | 
			
		||||
            _add_ip_addresses_to_addrinfo(cached, port, res)
 | 
			
		||||
            # Found in cache
 | 
			
		||||
            cached_addresses.extend(cached)
 | 
			
		||||
        else:
 | 
			
		||||
            # Not cached, need to resolve
 | 
			
		||||
            if address_cache and address_cache.has_cache():
 | 
			
		||||
                _LOGGER.info("Host %s not in cache, will need to resolve", h)
 | 
			
		||||
            uncached_hosts.append(h)
 | 
			
		||||
 | 
			
		||||
    # Process cached addresses (includes direct IPs and cached lookups)
 | 
			
		||||
    _add_ip_addresses_to_addrinfo(cached_addresses, port, res)
 | 
			
		||||
 | 
			
		||||
    # If we have uncached hosts (only non-IP hostnames), resolve them
 | 
			
		||||
    if uncached_hosts:
 | 
			
		||||
        from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo
 | 
			
		||||
 | 
			
		||||
        from esphome.core import EsphomeError
 | 
			
		||||
        from esphome.resolver import AsyncResolver
 | 
			
		||||
 | 
			
		||||
        resolver = AsyncResolver(uncached_hosts, port)
 | 
			
		||||
        addr_infos: list[AioAddrInfo] = []
 | 
			
		||||
        try:
 | 
			
		||||
            addr_infos = resolver.resolve()
 | 
			
		||||
        except EsphomeError as err:
 | 
			
		||||
            if not res:
 | 
			
		||||
                # No pre-resolved addresses available, DNS resolution is fatal
 | 
			
		||||
                raise
 | 
			
		||||
            _LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res))
 | 
			
		||||
 | 
			
		||||
        addr_infos = resolver.resolve()
 | 
			
		||||
        # Convert aioesphomeapi AddrInfo to our format
 | 
			
		||||
        for addr_info in addr_infos:
 | 
			
		||||
            sockaddr = addr_info.sockaddr
 | 
			
		||||
 
 | 
			
		||||
@@ -12,17 +12,16 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 | 
			
		||||
esptool==5.1.0
 | 
			
		||||
click==8.1.7
 | 
			
		||||
esphome-dashboard==20251013.0
 | 
			
		||||
aioesphomeapi==42.3.0
 | 
			
		||||
aioesphomeapi==42.2.0
 | 
			
		||||
zeroconf==0.148.0
 | 
			
		||||
puremagic==1.30
 | 
			
		||||
ruamel.yaml==0.18.16 # dashboard_import
 | 
			
		||||
ruamel.yaml==0.18.15 # dashboard_import
 | 
			
		||||
ruamel.yaml.clib==0.2.14 # dashboard_import
 | 
			
		||||
esphome-glyphsets==0.2.0
 | 
			
		||||
pillow==11.3.0
 | 
			
		||||
cairosvg==2.8.2
 | 
			
		||||
freetype-py==2.5.1
 | 
			
		||||
jinja2==3.1.6
 | 
			
		||||
bleak==1.1.1
 | 
			
		||||
 | 
			
		||||
# esp-idf >= 5.0 requires this
 | 
			
		||||
pyparsing >= 3.0
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,6 @@ ISOLATED_COMPONENTS = {
 | 
			
		||||
    "esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged",
 | 
			
		||||
    "ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
 | 
			
		||||
    "ethernet_info": "Related to ethernet component which conflicts with wifi",
 | 
			
		||||
    "gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas",
 | 
			
		||||
    "lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs",
 | 
			
		||||
    "mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle",
 | 
			
		||||
    "openthread": "Conflicts with wifi: used by most components",
 | 
			
		||||
 
 | 
			
		||||
@@ -336,7 +336,7 @@ def _component_has_tests(component: str) -> bool:
 | 
			
		||||
    Returns:
 | 
			
		||||
        True if the component has test YAML files
 | 
			
		||||
    """
 | 
			
		||||
    return bool(get_component_test_files(component, all_variants=True))
 | 
			
		||||
    return bool(get_component_test_files(component))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _select_platform_by_preference(
 | 
			
		||||
@@ -496,7 +496,7 @@ def detect_memory_impact_config(
 | 
			
		||||
 | 
			
		||||
    for component in sorted(changed_component_set):
 | 
			
		||||
        # Look for test files on preferred platforms
 | 
			
		||||
        test_files = get_component_test_files(component, all_variants=True)
 | 
			
		||||
        test_files = get_component_test_files(component)
 | 
			
		||||
        if not test_files:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,9 +49,9 @@ def has_test_files(component_name: str, tests_dir: Path) -> bool:
 | 
			
		||||
        tests_dir: Path to tests/components directory (unused, kept for compatibility)
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        True if the component has test.*.yaml or test-*.yaml files
 | 
			
		||||
        True if the component has test.*.yaml files
 | 
			
		||||
    """
 | 
			
		||||
    return bool(get_component_test_files(component_name, all_variants=True))
 | 
			
		||||
    return bool(get_component_test_files(component_name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_intelligent_batches(
 | 
			
		||||
@@ -118,13 +118,8 @@ def create_intelligent_batches(
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        # Get signature from any platform (they should all have the same buses)
 | 
			
		||||
        # Components not in component_buses may only have variant-specific tests
 | 
			
		||||
        comp_platforms = component_buses.get(component)
 | 
			
		||||
        if not comp_platforms:
 | 
			
		||||
            # Component has tests but no analyzable base config - treat as no buses
 | 
			
		||||
            signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component)
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        # Components not in component_buses were filtered out by has_test_files check
 | 
			
		||||
        comp_platforms = component_buses[component]
 | 
			
		||||
        for platform, buses in comp_platforms.items():
 | 
			
		||||
            if buses:
 | 
			
		||||
                signature = create_grouping_signature({platform: buses}, platform)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: DP83848
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: IP101
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: JL1101
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: KSZ8081
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: KSZ8081RNA
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: LAN8670
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: LAN8720
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: RTL8201
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
ethernet:
 | 
			
		||||
  type: LAN8720
 | 
			
		||||
  mdc_pin: 23
 | 
			
		||||
  mdio_pin: 32
 | 
			
		||||
  mdio_pin: 25
 | 
			
		||||
  clk:
 | 
			
		||||
    pin: 0
 | 
			
		||||
    mode: CLK_EXT_IN
 | 
			
		||||
  phy_addr: 0
 | 
			
		||||
  power_pin: 33
 | 
			
		||||
  power_pin: 26
 | 
			
		||||
  manual_ip:
 | 
			
		||||
    static_ip: 192.168.178.56
 | 
			
		||||
    gateway: 192.168.178.1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
fan:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    id: test_fan
 | 
			
		||||
    name: "Test Fan"
 | 
			
		||||
    preset_modes:
 | 
			
		||||
      - Eco
 | 
			
		||||
      - Sleep
 | 
			
		||||
      - Turbo
 | 
			
		||||
    has_oscillating: true
 | 
			
		||||
    has_direction: true
 | 
			
		||||
    speed_count: 3
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
sensor:
 | 
			
		||||
  - platform: hdc2010
 | 
			
		||||
    i2c_id: i2c_bus
 | 
			
		||||
    temperature:
 | 
			
		||||
      name: Temperature
 | 
			
		||||
    humidity:
 | 
			
		||||
      name: Humidity
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
wifi:
 | 
			
		||||
  ssid: MySSID
 | 
			
		||||
  password: password1
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
  hardware_uart: UART0
 | 
			
		||||
 | 
			
		||||
improv_serial:
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<<: !include common-uart0.yaml
 | 
			
		||||
@@ -574,105 +574,6 @@ def test_main_filters_components_without_tests(
 | 
			
		||||
    assert output["memory_impact"]["should_run"] == "false"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_main_detects_components_with_variant_tests(
 | 
			
		||||
    mock_should_run_integration_tests: Mock,
 | 
			
		||||
    mock_should_run_clang_tidy: Mock,
 | 
			
		||||
    mock_should_run_clang_format: Mock,
 | 
			
		||||
    mock_should_run_python_linters: Mock,
 | 
			
		||||
    mock_changed_files: Mock,
 | 
			
		||||
    capsys: pytest.CaptureFixture[str],
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    monkeypatch: pytest.MonkeyPatch,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test that components with only variant test files (test-*.yaml) are detected.
 | 
			
		||||
 | 
			
		||||
    This test verifies the fix for components like improv_serial, ethernet, mdns,
 | 
			
		||||
    improv_base, and safe_mode which only have variant test files (test-*.yaml)
 | 
			
		||||
    instead of base test files (test.*.yaml).
 | 
			
		||||
    """
 | 
			
		||||
    # Ensure we're not in GITHUB_ACTIONS mode for this test
 | 
			
		||||
    monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
 | 
			
		||||
 | 
			
		||||
    mock_should_run_integration_tests.return_value = False
 | 
			
		||||
    mock_should_run_clang_tidy.return_value = False
 | 
			
		||||
    mock_should_run_clang_format.return_value = False
 | 
			
		||||
    mock_should_run_python_linters.return_value = False
 | 
			
		||||
 | 
			
		||||
    # Mock changed_files to return component files
 | 
			
		||||
    mock_changed_files.return_value = [
 | 
			
		||||
        "esphome/components/improv_serial/improv_serial.cpp",
 | 
			
		||||
        "esphome/components/ethernet/ethernet.cpp",
 | 
			
		||||
        "esphome/components/no_tests/component.cpp",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # Create test directory structure
 | 
			
		||||
    tests_dir = tmp_path / "tests" / "components"
 | 
			
		||||
 | 
			
		||||
    # improv_serial has only variant tests (like the real component)
 | 
			
		||||
    improv_serial_dir = tests_dir / "improv_serial"
 | 
			
		||||
    improv_serial_dir.mkdir(parents=True)
 | 
			
		||||
    (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: config")
 | 
			
		||||
    (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: config")
 | 
			
		||||
    (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: config")
 | 
			
		||||
 | 
			
		||||
    # ethernet also has only variant tests
 | 
			
		||||
    ethernet_dir = tests_dir / "ethernet"
 | 
			
		||||
    ethernet_dir.mkdir(parents=True)
 | 
			
		||||
    (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: config")
 | 
			
		||||
    (ethernet_dir / "test-dhcp.esp32-idf.yaml").write_text("test: config")
 | 
			
		||||
 | 
			
		||||
    # no_tests component has no test files at all
 | 
			
		||||
    no_tests_dir = tests_dir / "no_tests"
 | 
			
		||||
    no_tests_dir.mkdir(parents=True)
 | 
			
		||||
 | 
			
		||||
    # Mock root_path to use tmp_path (need to patch both determine_jobs and helpers)
 | 
			
		||||
    with (
 | 
			
		||||
        patch.object(determine_jobs, "root_path", str(tmp_path)),
 | 
			
		||||
        patch.object(helpers, "root_path", str(tmp_path)),
 | 
			
		||||
        patch("sys.argv", ["determine-jobs.py"]),
 | 
			
		||||
        patch.object(
 | 
			
		||||
            determine_jobs,
 | 
			
		||||
            "get_changed_components",
 | 
			
		||||
            return_value=["improv_serial", "ethernet", "no_tests"],
 | 
			
		||||
        ),
 | 
			
		||||
        patch.object(
 | 
			
		||||
            determine_jobs,
 | 
			
		||||
            "filter_component_and_test_files",
 | 
			
		||||
            side_effect=lambda f: f.startswith("esphome/components/"),
 | 
			
		||||
        ),
 | 
			
		||||
        patch.object(
 | 
			
		||||
            determine_jobs,
 | 
			
		||||
            "get_components_with_dependencies",
 | 
			
		||||
            side_effect=lambda files, deps: (
 | 
			
		||||
                ["improv_serial", "ethernet"]
 | 
			
		||||
                if not deps
 | 
			
		||||
                else ["improv_serial", "ethernet", "no_tests"]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        patch.object(determine_jobs, "changed_files", return_value=[]),
 | 
			
		||||
    ):
 | 
			
		||||
        # Clear the cache since we're mocking root_path
 | 
			
		||||
        determine_jobs._component_has_tests.cache_clear()
 | 
			
		||||
        determine_jobs.main()
 | 
			
		||||
 | 
			
		||||
    # Check output
 | 
			
		||||
    captured = capsys.readouterr()
 | 
			
		||||
    output = json.loads(captured.out)
 | 
			
		||||
 | 
			
		||||
    # changed_components should have all components
 | 
			
		||||
    assert set(output["changed_components"]) == {
 | 
			
		||||
        "improv_serial",
 | 
			
		||||
        "ethernet",
 | 
			
		||||
        "no_tests",
 | 
			
		||||
    }
 | 
			
		||||
    # changed_components_with_tests should include components with variant tests
 | 
			
		||||
    assert set(output["changed_components_with_tests"]) == {"improv_serial", "ethernet"}
 | 
			
		||||
    # component_test_count should be 2 (improv_serial and ethernet)
 | 
			
		||||
    assert output["component_test_count"] == 2
 | 
			
		||||
    # no_tests should be excluded since it has no test files
 | 
			
		||||
    assert "no_tests" not in output["changed_components_with_tests"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Tests for detect_memory_impact_config function
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -884,51 +785,6 @@ def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) -
 | 
			
		||||
    assert "i2c" not in result["components"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None:
 | 
			
		||||
    """Test memory impact detection for components with only variant test files.
 | 
			
		||||
 | 
			
		||||
    This verifies that memory impact analysis works correctly for components like
 | 
			
		||||
    improv_serial, ethernet, mdns, etc. which only have variant test files
 | 
			
		||||
    (test-*.yaml) instead of base test files (test.*.yaml).
 | 
			
		||||
    """
 | 
			
		||||
    # Create test directory structure
 | 
			
		||||
    tests_dir = tmp_path / "tests" / "components"
 | 
			
		||||
 | 
			
		||||
    # improv_serial with only variant tests
 | 
			
		||||
    improv_serial_dir = tests_dir / "improv_serial"
 | 
			
		||||
    improv_serial_dir.mkdir(parents=True)
 | 
			
		||||
    (improv_serial_dir / "test-uart0.esp32-idf.yaml").write_text("test: improv")
 | 
			
		||||
    (improv_serial_dir / "test-uart0.esp8266-ard.yaml").write_text("test: improv")
 | 
			
		||||
    (improv_serial_dir / "test-usb_cdc.esp32-s2-idf.yaml").write_text("test: improv")
 | 
			
		||||
 | 
			
		||||
    # ethernet with only variant tests
 | 
			
		||||
    ethernet_dir = tests_dir / "ethernet"
 | 
			
		||||
    ethernet_dir.mkdir(parents=True)
 | 
			
		||||
    (ethernet_dir / "test-manual_ip.esp32-idf.yaml").write_text("test: ethernet")
 | 
			
		||||
    (ethernet_dir / "test-dhcp.esp32-c3-idf.yaml").write_text("test: ethernet")
 | 
			
		||||
 | 
			
		||||
    # Mock changed_files to return both components
 | 
			
		||||
    with (
 | 
			
		||||
        patch.object(determine_jobs, "root_path", str(tmp_path)),
 | 
			
		||||
        patch.object(helpers, "root_path", str(tmp_path)),
 | 
			
		||||
        patch.object(determine_jobs, "changed_files") as mock_changed_files,
 | 
			
		||||
    ):
 | 
			
		||||
        mock_changed_files.return_value = [
 | 
			
		||||
            "esphome/components/improv_serial/improv_serial.cpp",
 | 
			
		||||
            "esphome/components/ethernet/ethernet.cpp",
 | 
			
		||||
        ]
 | 
			
		||||
        determine_jobs._component_has_tests.cache_clear()
 | 
			
		||||
 | 
			
		||||
        result = determine_jobs.detect_memory_impact_config()
 | 
			
		||||
 | 
			
		||||
    # Should detect both components even though they only have variant tests
 | 
			
		||||
    assert result["should_run"] == "true"
 | 
			
		||||
    assert set(result["components"]) == {"improv_serial", "ethernet"}
 | 
			
		||||
    # Both components support esp32-idf
 | 
			
		||||
    assert result["platform"] == "esp32-idf"
 | 
			
		||||
    assert result["use_merged_config"] == "true"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Tests for clang-tidy split mode logic
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -571,11 +571,9 @@ class TestEsphomeCore:
 | 
			
		||||
        assert target.address == "4.3.2.1"
 | 
			
		||||
 | 
			
		||||
    def test_address__openthread(self, target):
 | 
			
		||||
        target.config = {}
 | 
			
		||||
        target.config[const.CONF_OPENTHREAD] = {
 | 
			
		||||
            const.CONF_USE_ADDRESS: "test-device.local"
 | 
			
		||||
        }
 | 
			
		||||
        target.name = "test-device"
 | 
			
		||||
        target.config = {}
 | 
			
		||||
        target.config[const.CONF_OPENTHREAD] = {}
 | 
			
		||||
 | 
			
		||||
        assert target.address == "test-device.local"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -454,27 +454,9 @@ def test_resolve_ip_address_mixed_list() -> None:
 | 
			
		||||
        # Mix of IP and hostname - should use async resolver
 | 
			
		||||
        result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
 | 
			
		||||
 | 
			
		||||
        assert len(result) == 2
 | 
			
		||||
        assert result[0][4][0] == "192.168.1.100"
 | 
			
		||||
        assert result[1][4][0] == "192.168.1.200"
 | 
			
		||||
        MockResolver.assert_called_once_with(["test.local"], 6053)
 | 
			
		||||
        mock_resolver.resolve.assert_called_once()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_resolve_ip_address_mixed_list_fail() -> None:
 | 
			
		||||
    """Test resolving a mix of IPs and hostnames with resolve failed."""
 | 
			
		||||
    with patch("esphome.resolver.AsyncResolver") as MockResolver:
 | 
			
		||||
        mock_resolver = MockResolver.return_value
 | 
			
		||||
        mock_resolver.resolve.side_effect = EsphomeError(
 | 
			
		||||
            "Error resolving IP address: [test.local]"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Mix of IP and hostname - should use async resolver
 | 
			
		||||
        result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
 | 
			
		||||
 | 
			
		||||
        assert len(result) == 1
 | 
			
		||||
        assert result[0][4][0] == "192.168.1.100"
 | 
			
		||||
        MockResolver.assert_called_once_with(["test.local"], 6053)
 | 
			
		||||
        assert result[0][4][0] == "192.168.1.200"
 | 
			
		||||
        MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053)
 | 
			
		||||
        mock_resolver.resolve.assert_called_once()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user