mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'entity_name_must_be_unique' into integration
This commit is contained in:
		| @@ -23,8 +23,10 @@ from esphome.const import ( | ||||
|     CONF_INTERRUPT_PIN, | ||||
|     CONF_MANUAL_IP, | ||||
|     CONF_MISO_PIN, | ||||
|     CONF_MODE, | ||||
|     CONF_MOSI_PIN, | ||||
|     CONF_PAGE_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_POLLING_INTERVAL, | ||||
|     CONF_RESET_PIN, | ||||
|     CONF_SPI, | ||||
| @@ -49,6 +51,7 @@ PHYRegister = ethernet_ns.struct("PHYRegister") | ||||
| CONF_PHY_ADDR = "phy_addr" | ||||
| CONF_MDC_PIN = "mdc_pin" | ||||
| CONF_MDIO_PIN = "mdio_pin" | ||||
| CONF_CLK = "clk" | ||||
| CONF_CLK_MODE = "clk_mode" | ||||
| CONF_POWER_PIN = "power_pin" | ||||
| CONF_PHY_REGISTERS = "phy_registers" | ||||
| @@ -73,26 +76,18 @@ SPI_ETHERNET_TYPES = ["W5500", "DM9051"] | ||||
| SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10) | ||||
|  | ||||
| emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t") | ||||
| emac_rmii_clock_gpio_t = cg.global_ns.enum("emac_rmii_clock_gpio_t") | ||||
|  | ||||
| CLK_MODES = { | ||||
|     "GPIO0_IN": ( | ||||
|         emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, | ||||
|         emac_rmii_clock_gpio_t.EMAC_CLK_IN_GPIO, | ||||
|     ), | ||||
|     "GPIO0_OUT": ( | ||||
|         emac_rmii_clock_mode_t.EMAC_CLK_OUT, | ||||
|         emac_rmii_clock_gpio_t.EMAC_APPL_CLK_OUT_GPIO, | ||||
|     ), | ||||
|     "GPIO16_OUT": ( | ||||
|         emac_rmii_clock_mode_t.EMAC_CLK_OUT, | ||||
|         emac_rmii_clock_gpio_t.EMAC_CLK_OUT_GPIO, | ||||
|     ), | ||||
|     "GPIO17_OUT": ( | ||||
|         emac_rmii_clock_mode_t.EMAC_CLK_OUT, | ||||
|         emac_rmii_clock_gpio_t.EMAC_CLK_OUT_180_GPIO, | ||||
|     ), | ||||
|     "CLK_EXT_IN": emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN, | ||||
|     "CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT, | ||||
| } | ||||
|  | ||||
| CLK_MODES_DEPRECATED = { | ||||
|     "GPIO0_IN": ("CLK_EXT_IN", 0), | ||||
|     "GPIO0_OUT": ("CLK_OUT", 0), | ||||
|     "GPIO16_OUT": ("CLK_OUT", 16), | ||||
|     "GPIO17_OUT": ("CLK_OUT", 17), | ||||
| } | ||||
|  | ||||
| MANUAL_IP_SCHEMA = cv.Schema( | ||||
|     { | ||||
| @@ -154,6 +149,18 @@ def _validate(config): | ||||
|                     f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " | ||||
|                     f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." | ||||
|                 ) | ||||
|     elif config[CONF_TYPE] != "OPENETH": | ||||
|         if CONF_CLK_MODE in config: | ||||
|             LOGGER.warning( | ||||
|                 "[ethernet] The 'clk_mode' option is deprecated and will be removed in ESPHome 2026.1. " | ||||
|                 "Please update your configuration to use 'clk' instead." | ||||
|             ) | ||||
|             mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]] | ||||
|             config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]}) | ||||
|             del config[CONF_CLK_MODE] | ||||
|         elif CONF_CLK not in config: | ||||
|             raise cv.Invalid("'clk' is a required option for [ethernet].") | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| @@ -177,14 +184,21 @@ PHY_REGISTER_SCHEMA = cv.Schema( | ||||
|         cv.Optional(CONF_PAGE_ID): cv.hex_int, | ||||
|     } | ||||
| ) | ||||
| CLK_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_MODE): cv.enum(CLK_MODES, upper=True, space="_"), | ||||
|         cv.Required(CONF_PIN): pins.internal_gpio_pin_number, | ||||
|     } | ||||
| ) | ||||
| RMII_SCHEMA = BASE_SCHEMA.extend( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Optional(CONF_CLK_MODE, default="GPIO0_IN"): cv.enum( | ||||
|                 CLK_MODES, upper=True, space="_" | ||||
|             cv.Optional(CONF_CLK_MODE): cv.enum( | ||||
|                 CLK_MODES_DEPRECATED, upper=True, space="_" | ||||
|             ), | ||||
|             cv.Optional(CONF_CLK): CLK_SCHEMA, | ||||
|             cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), | ||||
|             cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA), | ||||
| @@ -308,7 +322,8 @@ async def to_code(config): | ||||
|         cg.add(var.set_phy_addr(config[CONF_PHY_ADDR])) | ||||
|         cg.add(var.set_mdc_pin(config[CONF_MDC_PIN])) | ||||
|         cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN])) | ||||
|         cg.add(var.set_clk_mode(*CLK_MODES[config[CONF_CLK_MODE]])) | ||||
|         cg.add(var.set_clk_mode(config[CONF_CLK][CONF_MODE])) | ||||
|         cg.add(var.set_clk_pin(config[CONF_CLK][CONF_PIN])) | ||||
|         if CONF_POWER_PIN in config: | ||||
|             cg.add(var.set_power_pin(config[CONF_POWER_PIN])) | ||||
|         for register_value in config.get(CONF_PHY_REGISTERS, []): | ||||
|   | ||||
| @@ -17,6 +17,22 @@ | ||||
| namespace esphome { | ||||
| namespace ethernet { | ||||
|  | ||||
| #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) | ||||
| // work around IDF compile issue on P4 https://github.com/espressif/esp-idf/pull/15637 | ||||
| #ifdef USE_ESP32_VARIANT_ESP32P4 | ||||
| #undef ETH_ESP32_EMAC_DEFAULT_CONFIG | ||||
| #define ETH_ESP32_EMAC_DEFAULT_CONFIG() \ | ||||
|   { \ | ||||
|     .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, .interface = EMAC_DATA_INTERFACE_RMII, \ | ||||
|     .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) 50}}, \ | ||||
|     .dma_burst_len = ETH_DMA_BURST_LEN_32, .intr_priority = 0, \ | ||||
|     .emac_dataif_gpio = \ | ||||
|         {.rmii = {.tx_en_num = 49, .txd0_num = 34, .txd1_num = 35, .crs_dv_num = 28, .rxd0_num = 29, .rxd1_num = 30}}, \ | ||||
|     .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = (emac_rmii_clock_gpio_t) -1}}, \ | ||||
|   } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| static const char *const TAG = "ethernet"; | ||||
|  | ||||
| EthernetComponent *global_eth_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| @@ -150,22 +166,18 @@ void EthernetComponent::setup() { | ||||
|   phy_config.phy_addr = this->phy_addr_; | ||||
|   phy_config.reset_gpio_num = this->power_pin_; | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG(); | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) | ||||
|   esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_; | ||||
|   esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_; | ||||
| #else | ||||
|   esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_; | ||||
|   esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_; | ||||
| #endif | ||||
|   esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_; | ||||
|   esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; | ||||
|   esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_; | ||||
|  | ||||
|   esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); | ||||
| #else | ||||
|   mac_config.smi_mdc_gpio_num = this->mdc_pin_; | ||||
|   mac_config.smi_mdio_gpio_num = this->mdio_pin_; | ||||
|   mac_config.clock_config.rmii.clock_mode = this->clk_mode_; | ||||
|   mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_; | ||||
|  | ||||
|   esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config); | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
|   switch (this->type_) { | ||||
| @@ -387,10 +399,11 @@ void EthernetComponent::dump_config() { | ||||
|     ESP_LOGCONFIG(TAG, "  Power Pin: %u", this->power_pin_); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  CLK Pin: %u\n" | ||||
|                 "  MDC Pin: %u\n" | ||||
|                 "  MDIO Pin: %u\n" | ||||
|                 "  PHY addr: %u", | ||||
|                 this->mdc_pin_, this->mdio_pin_, this->phy_addr_); | ||||
|                 this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "  Type: %s", eth_type); | ||||
| } | ||||
| @@ -611,10 +624,8 @@ void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_a | ||||
| void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; } | ||||
| void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } | ||||
| void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; } | ||||
| void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio) { | ||||
|   this->clk_mode_ = clk_mode; | ||||
|   this->clk_gpio_ = clk_gpio; | ||||
| } | ||||
| void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; } | ||||
| void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } | ||||
| void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } | ||||
| #endif | ||||
| void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } | ||||
|   | ||||
| @@ -76,7 +76,8 @@ class EthernetComponent : public Component { | ||||
|   void set_power_pin(int power_pin); | ||||
|   void set_mdc_pin(uint8_t mdc_pin); | ||||
|   void set_mdio_pin(uint8_t mdio_pin); | ||||
|   void set_clk_mode(emac_rmii_clock_mode_t clk_mode, emac_rmii_clock_gpio_t clk_gpio); | ||||
|   void set_clk_pin(uint8_t clk_pin); | ||||
|   void set_clk_mode(emac_rmii_clock_mode_t clk_mode); | ||||
|   void add_phy_register(PHYRegister register_value); | ||||
| #endif | ||||
|   void set_type(EthernetType type); | ||||
| @@ -123,10 +124,10 @@ class EthernetComponent : public Component { | ||||
|   // Group all 32-bit members first | ||||
|   int power_pin_{-1}; | ||||
|   emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; | ||||
|   emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; | ||||
|   std::vector<PHYRegister> phy_registers_{}; | ||||
|  | ||||
|   // Group all 8-bit members together | ||||
|   uint8_t clk_pin_{0}; | ||||
|   uint8_t phy_addr_{0}; | ||||
|   uint8_t mdc_pin_{23}; | ||||
|   uint8_t mdio_pin_{18}; | ||||
|   | ||||
| @@ -57,7 +57,7 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   uint8_t *data = allocator.allocate(container->content_length); | ||||
|   if (data == nullptr) { | ||||
|     std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); | ||||
|     std::string msg = str_sprintf("Failed to allocate %zu bytes for manifest", container->content_length); | ||||
|     this_update->status_set_error(msg.c_str()); | ||||
|     container->end(); | ||||
|     UPDATE_RETURN; | ||||
|   | ||||
| @@ -9,8 +9,6 @@ from esphome.const import ( | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_I2C_ID, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_OUTPUT, | ||||
|     CONF_SCAN, | ||||
|     CONF_SCL, | ||||
|     CONF_SDA, | ||||
| @@ -73,20 +71,15 @@ def validate_config(config): | ||||
|     return config | ||||
|  | ||||
|  | ||||
| pin_with_input_and_output_support = pins.internal_gpio_pin_number( | ||||
|     {CONF_OUTPUT: True, CONF_INPUT: True} | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): _bus_declare_type, | ||||
|             cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, | ||||
|             cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number, | ||||
|             cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All( | ||||
|                 cv.only_with_esp_idf, cv.boolean | ||||
|             ), | ||||
|             cv.Optional(CONF_SCL, default="SCL"): pin_with_input_and_output_support, | ||||
|             cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number, | ||||
|             cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( | ||||
|                 cv.only_with_esp_idf, cv.boolean | ||||
|             ), | ||||
|   | ||||
| @@ -163,12 +163,20 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) { | ||||
|     case MQTT_EVENT_CONNECTED: | ||||
|       ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); | ||||
|       this->is_connected_ = true; | ||||
| #if defined(USE_MQTT_IDF_ENQUEUE) | ||||
|       this->last_dropped_log_time_ = 0; | ||||
|       xTaskNotifyGive(this->task_handle_); | ||||
| #endif | ||||
|       this->on_connect_.call(event.session_present); | ||||
|       break; | ||||
|     case MQTT_EVENT_DISCONNECTED: | ||||
|       ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); | ||||
|       // TODO is there a way to get the disconnect reason? | ||||
|       this->is_connected_ = false; | ||||
| #if defined(USE_MQTT_IDF_ENQUEUE) | ||||
|       this->last_dropped_log_time_ = 0; | ||||
|       xTaskNotifyGive(this->task_handle_); | ||||
| #endif | ||||
|       this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); | ||||
|       break; | ||||
|  | ||||
|   | ||||
| @@ -116,7 +116,7 @@ struct QueueElement { | ||||
| class MQTTBackendESP32 final : public MQTTBackend { | ||||
|  public: | ||||
|   static const size_t MQTT_BUFFER_SIZE = 4096; | ||||
|   static const size_t TASK_STACK_SIZE = 2048; | ||||
|   static const size_t TASK_STACK_SIZE = 3072; | ||||
|   static const size_t TASK_STACK_SIZE_TLS = 4096;  // Larger stack for TLS operations | ||||
|   static const ssize_t TASK_PRIORITY = 5; | ||||
|   static const uint8_t MQTT_QUEUE_LENGTH = 30;  // 30*12 bytes = 360 | ||||
|   | ||||
| @@ -22,7 +22,6 @@ from .const import ( | ||||
|     CONF_SRP_ID, | ||||
|     CONF_TLV, | ||||
| ) | ||||
| from .tlv import parse_tlv | ||||
|  | ||||
| CODEOWNERS = ["@mrene"] | ||||
|  | ||||
| @@ -43,29 +42,40 @@ def set_sdkconfig_options(config): | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_CLI", False) | ||||
|  | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID]) | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL]) | ||||
|     add_idf_sdkconfig_option( | ||||
|         "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower() | ||||
|     ) | ||||
|  | ||||
|     if network_name := config.get(CONF_NETWORK_NAME): | ||||
|         add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) | ||||
|     if tlv := config.get(CONF_TLV): | ||||
|         cg.add_define("USE_OPENTHREAD_TLVS", tlv) | ||||
|     else: | ||||
|         if pan_id := config.get(CONF_PAN_ID): | ||||
|             add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id) | ||||
|  | ||||
|     if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() | ||||
|         ) | ||||
|     if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() | ||||
|         ) | ||||
|     if (pskc := config.get(CONF_PSKC)) is not None: | ||||
|         add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower()) | ||||
|         if channel := config.get(CONF_CHANNEL): | ||||
|             add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", channel) | ||||
|  | ||||
|     if CONF_FORCE_DATASET in config: | ||||
|         if config[CONF_FORCE_DATASET]: | ||||
|             cg.add_define("CONFIG_OPENTHREAD_FORCE_DATASET") | ||||
|         if network_key := config.get(CONF_NETWORK_KEY): | ||||
|             add_idf_sdkconfig_option( | ||||
|                 "CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{network_key:X}".lower() | ||||
|             ) | ||||
|  | ||||
|         if network_name := config.get(CONF_NETWORK_NAME): | ||||
|             add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_NAME", network_name) | ||||
|  | ||||
|         if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None: | ||||
|             add_idf_sdkconfig_option( | ||||
|                 "CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower() | ||||
|             ) | ||||
|         if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None: | ||||
|             add_idf_sdkconfig_option( | ||||
|                 "CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower() | ||||
|             ) | ||||
|         if (pskc := config.get(CONF_PSKC)) is not None: | ||||
|             add_idf_sdkconfig_option( | ||||
|                 "CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower() | ||||
|             ) | ||||
|  | ||||
|     if force_dataset := config.get(CONF_FORCE_DATASET): | ||||
|         if force_dataset: | ||||
|             cg.add_define("USE_OPENTHREAD_FORCE_DATASET") | ||||
|  | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_DNS64_CLIENT", True) | ||||
|     add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) | ||||
| @@ -79,22 +89,11 @@ openthread_ns = cg.esphome_ns.namespace("openthread") | ||||
| OpenThreadComponent = openthread_ns.class_("OpenThreadComponent", cg.Component) | ||||
| OpenThreadSrpComponent = openthread_ns.class_("OpenThreadSrpComponent", cg.Component) | ||||
|  | ||||
|  | ||||
| def _convert_tlv(config): | ||||
|     if tlv := config.get(CONF_TLV): | ||||
|         config = config.copy() | ||||
|         parsed_tlv = parse_tlv(tlv) | ||||
|         validated = _CONNECTION_SCHEMA(parsed_tlv) | ||||
|         config.update(validated) | ||||
|         del config[CONF_TLV] | ||||
|     return config | ||||
|  | ||||
|  | ||||
| _CONNECTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Inclusive(CONF_PAN_ID, "manual"): cv.hex_int, | ||||
|         cv.Inclusive(CONF_CHANNEL, "manual"): cv.int_, | ||||
|         cv.Inclusive(CONF_NETWORK_KEY, "manual"): cv.hex_int, | ||||
|         cv.Optional(CONF_PAN_ID): cv.hex_int, | ||||
|         cv.Optional(CONF_CHANNEL): cv.int_, | ||||
|         cv.Optional(CONF_NETWORK_KEY): cv.hex_int, | ||||
|         cv.Optional(CONF_EXT_PAN_ID): cv.hex_int, | ||||
|         cv.Optional(CONF_NETWORK_NAME): cv.string_strict, | ||||
|         cv.Optional(CONF_PSKC): cv.hex_int, | ||||
| @@ -112,8 +111,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_TLV): cv.string_strict, | ||||
|         } | ||||
|     ).extend(_CONNECTION_SCHEMA), | ||||
|     cv.has_exactly_one_key(CONF_PAN_ID, CONF_TLV), | ||||
|     _convert_tlv, | ||||
|     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), | ||||
|     cv.only_with_esp_idf, | ||||
|     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), | ||||
| ) | ||||
|   | ||||
| @@ -111,14 +111,36 @@ void OpenThreadComponent::ot_main() { | ||||
|   esp_openthread_cli_create_task(); | ||||
| #endif | ||||
|   ESP_LOGI(TAG, "Activating dataset..."); | ||||
|   otOperationalDatasetTlvs dataset; | ||||
|   otOperationalDatasetTlvs dataset = {}; | ||||
|  | ||||
| #ifdef CONFIG_OPENTHREAD_FORCE_DATASET | ||||
|   ESP_ERROR_CHECK(esp_openthread_auto_start(NULL)); | ||||
| #else | ||||
| #ifndef USE_OPENTHREAD_FORCE_DATASET | ||||
|   // Check if openthread has a valid dataset from a previous execution | ||||
|   otError error = otDatasetGetActiveTlvs(esp_openthread_get_instance(), &dataset); | ||||
|   ESP_ERROR_CHECK(esp_openthread_auto_start((error == OT_ERROR_NONE) ? &dataset : NULL)); | ||||
|   if (error != OT_ERROR_NONE) { | ||||
|     // Make sure the length is 0 so we fallback to the configuration | ||||
|     dataset.mLength = 0; | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "Found OpenThread-managed dataset, ignoring esphome configuration"); | ||||
|     ESP_LOGI(TAG, "(set force_dataset: true to override)"); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_OPENTHREAD_TLVS | ||||
|   if (dataset.mLength == 0) { | ||||
|     // If we didn't have an active dataset, and we have tlvs, parse it and pass it to esp_openthread_auto_start | ||||
|     size_t len = (sizeof(USE_OPENTHREAD_TLVS) - 1) / 2; | ||||
|     if (len > sizeof(dataset.mTlvs)) { | ||||
|       ESP_LOGW(TAG, "TLV buffer too small, truncating"); | ||||
|       len = sizeof(dataset.mTlvs); | ||||
|     } | ||||
|     parse_hex(USE_OPENTHREAD_TLVS, sizeof(USE_OPENTHREAD_TLVS) - 1, dataset.mTlvs, len); | ||||
|     dataset.mLength = len; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // Pass the existing dataset, or NULL which will use the preprocessor definitions | ||||
|   ESP_ERROR_CHECK(esp_openthread_auto_start(dataset.mLength > 0 ? &dataset : nullptr)); | ||||
|  | ||||
|   esp_openthread_launch_mainloop(); | ||||
|  | ||||
|   // Clean up | ||||
|   | ||||
| @@ -1,65 +0,0 @@ | ||||
| # Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9 | ||||
| import binascii | ||||
| import ipaddress | ||||
|  | ||||
| from esphome.const import CONF_CHANNEL | ||||
|  | ||||
| from . import ( | ||||
|     CONF_EXT_PAN_ID, | ||||
|     CONF_MESH_LOCAL_PREFIX, | ||||
|     CONF_NETWORK_KEY, | ||||
|     CONF_NETWORK_NAME, | ||||
|     CONF_PAN_ID, | ||||
|     CONF_PSKC, | ||||
| ) | ||||
|  | ||||
| TLV_TYPES = { | ||||
|     0: CONF_CHANNEL, | ||||
|     1: CONF_PAN_ID, | ||||
|     2: CONF_EXT_PAN_ID, | ||||
|     3: CONF_NETWORK_NAME, | ||||
|     4: CONF_PSKC, | ||||
|     5: CONF_NETWORK_KEY, | ||||
|     7: CONF_MESH_LOCAL_PREFIX, | ||||
| } | ||||
|  | ||||
|  | ||||
| def parse_tlv(tlv) -> dict: | ||||
|     data = binascii.a2b_hex(tlv) | ||||
|     output = {} | ||||
|     pos = 0 | ||||
|     while pos < len(data): | ||||
|         tag = data[pos] | ||||
|         pos += 1 | ||||
|         _len = data[pos] | ||||
|         pos += 1 | ||||
|         val = data[pos : pos + _len] | ||||
|         pos += _len | ||||
|         if tag in TLV_TYPES: | ||||
|             if tag == 3: | ||||
|                 output[TLV_TYPES[tag]] = val.decode("utf-8") | ||||
|             elif tag == 7: | ||||
|                 mesh_local_prefix = binascii.hexlify(val).decode("utf-8") | ||||
|                 mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000" | ||||
|                 ipv6_bytes = bytes.fromhex(mesh_local_prefix_str) | ||||
|                 ipv6_address = ipaddress.IPv6Address(ipv6_bytes) | ||||
|                 output[TLV_TYPES[tag]] = f"{ipv6_address}/64" | ||||
|             else: | ||||
|                 output[TLV_TYPES[tag]] = int.from_bytes(val) | ||||
|     return output | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     import sys | ||||
|  | ||||
|     args = sys.argv[1:] | ||||
|     parsed = parse_tlv(args[0]) | ||||
|     # print the parsed TLV data | ||||
|     for key, value in parsed.items(): | ||||
|         if isinstance(value, bytes): | ||||
|             value = value.hex() | ||||
|         print(f"{key}: {value}") | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| @@ -6,6 +6,7 @@ from esphome.const import ( | ||||
|     CONF_DIELECTRIC_CONSTANT, | ||||
|     CONF_ID, | ||||
|     CONF_MOISTURE, | ||||
|     CONF_PERMITTIVITY, | ||||
|     CONF_TEMPERATURE, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
| @@ -33,7 +34,10 @@ CONFIG_SCHEMA = ( | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_DIELECTRIC_CONSTANT): sensor.sensor_schema( | ||||
|             cv.Optional(CONF_DIELECTRIC_CONSTANT): cv.invalid( | ||||
|                 "Use 'permittivity' instead" | ||||
|             ), | ||||
|             cv.Optional(CONF_PERMITTIVITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_EMPTY, | ||||
|                 accuracy_decimals=2, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
| @@ -76,9 +80,9 @@ async def to_code(config): | ||||
|         sens = await sensor.new_sensor(config[CONF_COUNTS]) | ||||
|         cg.add(var.set_counts_sensor(sens)) | ||||
|  | ||||
|     if CONF_DIELECTRIC_CONSTANT in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_DIELECTRIC_CONSTANT]) | ||||
|         cg.add(var.set_dielectric_constant_sensor(sens)) | ||||
|     if CONF_PERMITTIVITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PERMITTIVITY]) | ||||
|         cg.add(var.set_permittivity_sensor(sens)) | ||||
|  | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|   | ||||
| @@ -16,7 +16,7 @@ void SMT100Component::loop() { | ||||
|   while (this->available() != 0) { | ||||
|     if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { | ||||
|       int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); | ||||
|       float dielectric_constant = (float) strtod((strtok(nullptr, ",")), nullptr); | ||||
|       float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); | ||||
|       float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); | ||||
|       float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); | ||||
|       float voltage = (float) strtod((strtok(nullptr, ",")), nullptr); | ||||
| @@ -25,8 +25,8 @@ void SMT100Component::loop() { | ||||
|         counts_sensor_->publish_state(counts); | ||||
|       } | ||||
|  | ||||
|       if (this->dielectric_constant_sensor_ != nullptr) { | ||||
|         dielectric_constant_sensor_->publish_state(dielectric_constant); | ||||
|       if (this->permittivity_sensor_ != nullptr) { | ||||
|         permittivity_sensor_->publish_state(permittivity); | ||||
|       } | ||||
|  | ||||
|       if (this->moisture_sensor_ != nullptr) { | ||||
| @@ -49,8 +49,8 @@ float SMT100Component::get_setup_priority() const { return setup_priority::DATA; | ||||
| void SMT100Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "SMT100:"); | ||||
|  | ||||
|   LOG_SENSOR(TAG, "Counts", this->temperature_sensor_); | ||||
|   LOG_SENSOR(TAG, "Dielectric Constant", this->temperature_sensor_); | ||||
|   LOG_SENSOR(TAG, "Counts", this->counts_sensor_); | ||||
|   LOG_SENSOR(TAG, "Permittivity", this->permittivity_sensor_); | ||||
|   LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   | ||||
| @@ -20,8 +20,8 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } | ||||
|   void set_dielectric_constant_sensor(sensor::Sensor *dielectric_constant_sensor) { | ||||
|     this->dielectric_constant_sensor_ = dielectric_constant_sensor; | ||||
|   void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { | ||||
|     this->permittivity_sensor_ = permittivity_sensor; | ||||
|   } | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } | ||||
|   void set_moisture_sensor(sensor::Sensor *moisture_sensor) { this->moisture_sensor_ = moisture_sensor; } | ||||
| @@ -31,7 +31,7 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { | ||||
|   int readline_(int readch, char *buffer, int len); | ||||
|  | ||||
|   sensor::Sensor *counts_sensor_{nullptr}; | ||||
|   sensor::Sensor *dielectric_constant_sensor_{nullptr}; | ||||
|   sensor::Sensor *permittivity_sensor_{nullptr}; | ||||
|   sensor::Sensor *moisture_sensor_{nullptr}; | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   | ||||
| @@ -654,6 +654,7 @@ CONF_PAYLOAD = "payload" | ||||
| CONF_PAYLOAD_AVAILABLE = "payload_available" | ||||
| CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" | ||||
| CONF_PERIOD = "period" | ||||
| CONF_PERMITTIVITY = "permittivity" | ||||
| CONF_PH = "ph" | ||||
| CONF_PHASE_A = "phase_a" | ||||
| CONF_PHASE_ANGLE = "phase_angle" | ||||
|   | ||||
| @@ -184,25 +184,18 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy | ||||
|             # No name to validate | ||||
|             return config | ||||
|  | ||||
|         # Get the entity name and device info | ||||
|         # Get the entity name | ||||
|         entity_name = config[CONF_NAME] | ||||
|         device_id = ""  # Empty string for main device | ||||
|  | ||||
|         if CONF_DEVICE_ID in config: | ||||
|             device_id_obj = config[CONF_DEVICE_ID] | ||||
|             # Use the device ID string directly for uniqueness | ||||
|             device_id = device_id_obj.id | ||||
|  | ||||
|         # For duplicate detection, just use the sanitized name | ||||
|         name_key = sanitize(snake_case(entity_name)) | ||||
|  | ||||
|         # Check for duplicates | ||||
|         unique_key = (device_id, platform, name_key) | ||||
|         unique_key = (platform, name_key) | ||||
|         if unique_key in CORE.unique_ids: | ||||
|             device_prefix = f" on device '{device_id}'" if device_id else "" | ||||
|             raise cv.Invalid( | ||||
|                 f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " | ||||
|                 f"Each entity on a device must have a unique name within its platform." | ||||
|                 f"Duplicate {platform} entity with name '{entity_name}' found. " | ||||
|                 f"Each entity must have a unique name within its platform across all devices." | ||||
|             ) | ||||
|  | ||||
|         # Add to tracking set | ||||
|   | ||||
| @@ -220,7 +220,9 @@ def gpio_flags_expr(mode): | ||||
|  | ||||
|  | ||||
| gpio_pin_schema = _schema_creator | ||||
| internal_gpio_pin_number = _internal_number_creator | ||||
| internal_gpio_pin_number = _internal_number_creator( | ||||
|     {CONF_OUTPUT: True, CONF_INPUT: True} | ||||
| ) | ||||
| gpio_output_pin_schema = _schema_creator( | ||||
|     { | ||||
|         CONF_OUTPUT: True, | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: DP83848 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: IP101 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: JL1101 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: KSZ8081 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: KSZ8081RNA | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: LAN8720 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: RTL8201 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -2,7 +2,9 @@ ethernet: | ||||
|   type: LAN8720 | ||||
|   mdc_pin: 23 | ||||
|   mdio_pin: 25 | ||||
|   clk_mode: GPIO0_IN | ||||
|   clk: | ||||
|     pin: 0 | ||||
|     mode: CLK_EXT_IN | ||||
|   phy_addr: 0 | ||||
|   power_pin: 26 | ||||
|   manual_ip: | ||||
|   | ||||
| @@ -8,8 +8,8 @@ sensor: | ||||
|   - platform: smt100 | ||||
|     counts: | ||||
|       name: Counts | ||||
|     dielectric_constant: | ||||
|       name: Dielectric Constant | ||||
|     permittivity: | ||||
|       name: Permittivity | ||||
|     temperature: | ||||
|       name: Temperature | ||||
|     moisture: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| esphome: | ||||
|   name: duplicate-entities-test | ||||
|   # Define devices to test multi-device duplicate handling | ||||
|   # Define devices to test multi-device unique name validation | ||||
|   devices: | ||||
|     - id: controller_1 | ||||
|       name: Controller 1 | ||||
| @@ -13,31 +13,31 @@ host: | ||||
| api:  # Port will be automatically injected | ||||
| logger: | ||||
| 
 | ||||
| # Test that duplicate entity names are allowed on different devices | ||||
| # Test that duplicate entity names are NOT allowed on different devices | ||||
| 
 | ||||
| # Scenario 1: Same sensor name on different devices (allowed) | ||||
| # Scenario 1: Different sensor names on different devices (allowed) | ||||
| sensor: | ||||
|   - platform: template | ||||
|     name: Temperature | ||||
|     name: Temperature Controller 1 | ||||
|     device_id: controller_1 | ||||
|     lambda: return 21.0; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Temperature | ||||
|     name: Temperature Controller 2 | ||||
|     device_id: controller_2 | ||||
|     lambda: return 22.0; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Temperature | ||||
|     name: Temperature Controller 3 | ||||
|     device_id: controller_3 | ||||
|     lambda: return 23.0; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
|   # Main device sensor (no device_id) | ||||
|   - platform: template | ||||
|     name: Temperature | ||||
|     name: Temperature Main | ||||
|     lambda: return 20.0; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
| @@ -47,20 +47,20 @@ sensor: | ||||
|     lambda: return 60.0; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
| # Scenario 2: Same binary sensor name on different devices (allowed) | ||||
| # Scenario 2: Different binary sensor names on different devices | ||||
| binary_sensor: | ||||
|   - platform: template | ||||
|     name: Status | ||||
|     name: Status Controller 1 | ||||
|     device_id: controller_1 | ||||
|     lambda: return true; | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Status | ||||
|     name: Status Controller 2 | ||||
|     device_id: controller_2 | ||||
|     lambda: return false; | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Status | ||||
|     name: Status Main | ||||
|     lambda: return true;  # Main device | ||||
| 
 | ||||
|   # Different platform can have same name as sensor | ||||
| @@ -68,43 +68,43 @@ binary_sensor: | ||||
|     name: Temperature | ||||
|     lambda: return true; | ||||
| 
 | ||||
| # Scenario 3: Same text sensor name on different devices | ||||
| # Scenario 3: Different text sensor names on different devices | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     name: Device Info | ||||
|     name: Device Info Controller 1 | ||||
|     device_id: controller_1 | ||||
|     lambda: return {"Controller 1 Active"}; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Device Info | ||||
|     name: Device Info Controller 2 | ||||
|     device_id: controller_2 | ||||
|     lambda: return {"Controller 2 Active"}; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Device Info | ||||
|     name: Device Info Main | ||||
|     lambda: return {"Main Device Active"}; | ||||
|     update_interval: 0.1s | ||||
| 
 | ||||
| # Scenario 4: Same switch name on different devices | ||||
| # Scenario 4: Different switch names on different devices | ||||
| switch: | ||||
|   - platform: template | ||||
|     name: Power | ||||
|     name: Power Controller 1 | ||||
|     device_id: controller_1 | ||||
|     lambda: return false; | ||||
|     turn_on_action: [] | ||||
|     turn_off_action: [] | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Power | ||||
|     name: Power Controller 2 | ||||
|     device_id: controller_2 | ||||
|     lambda: return true; | ||||
|     turn_on_action: [] | ||||
|     turn_off_action: [] | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: Power | ||||
|     name: Power Controller 3 | ||||
|     device_id: controller_3 | ||||
|     lambda: return false; | ||||
|     turn_on_action: [] | ||||
| @@ -117,26 +117,26 @@ switch: | ||||
|     turn_on_action: [] | ||||
|     turn_off_action: [] | ||||
| 
 | ||||
| # Scenario 5: Empty names on different devices (should use device name) | ||||
| # Scenario 5: Buttons with unique names | ||||
| button: | ||||
|   - platform: template | ||||
|     name: "" | ||||
|     name: "Reset Controller 1" | ||||
|     device_id: controller_1 | ||||
|     on_press: [] | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: "" | ||||
|     name: "Reset Controller 2" | ||||
|     device_id: controller_2 | ||||
|     on_press: [] | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: "" | ||||
|     name: "Reset Main" | ||||
|     on_press: []  # Main device | ||||
| 
 | ||||
| # Scenario 6: Special characters in names | ||||
| # Scenario 6: Special characters in names - now with unique names | ||||
| number: | ||||
|   - platform: template | ||||
|     name: "Temperature Setpoint!" | ||||
|     name: "Temperature Setpoint! Controller 1" | ||||
|     device_id: controller_1 | ||||
|     min_value: 10.0 | ||||
|     max_value: 30.0 | ||||
| @@ -145,7 +145,7 @@ number: | ||||
|     set_action: [] | ||||
| 
 | ||||
|   - platform: template | ||||
|     name: "Temperature Setpoint!" | ||||
|     name: "Temperature Setpoint! Controller 2" | ||||
|     device_id: controller_2 | ||||
|     min_value: 10.0 | ||||
|     max_value: 30.0 | ||||
| @@ -177,19 +177,22 @@ async def test_api_conditional_memory( | ||||
|         async with api_client_connected() as client2: | ||||
|             # Subscribe to states with new client | ||||
|             states2: dict[int, EntityState] = {} | ||||
|             connected_future: asyncio.Future[None] = loop.create_future() | ||||
|             states_ready_future: asyncio.Future[None] = loop.create_future() | ||||
|  | ||||
|             def on_state2(state: EntityState) -> None: | ||||
|                 states2[state.key] = state | ||||
|                 # Check for reconnection | ||||
|                 if state.key == client_connected.key and state.state is True: | ||||
|                     if not connected_future.done(): | ||||
|                         connected_future.set_result(None) | ||||
|                 # Check if we have received both required states | ||||
|                 if ( | ||||
|                     client_connected.key in states2 | ||||
|                     and client_disconnected_event.key in states2 | ||||
|                     and not states_ready_future.done() | ||||
|                 ): | ||||
|                     states_ready_future.set_result(None) | ||||
|  | ||||
|             client2.subscribe_states(on_state2) | ||||
|  | ||||
|             # Wait for connected state | ||||
|             await asyncio.wait_for(connected_future, timeout=5.0) | ||||
|             # Wait for both connected and disconnected event states | ||||
|             await asyncio.wait_for(states_ready_future, timeout=5.0) | ||||
|  | ||||
|             # Verify client is connected again (on_client_connected fired) | ||||
|             assert states2[client_connected.key].state is True, ( | ||||
|   | ||||
| @@ -11,12 +11,12 @@ from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_duplicate_entities_on_different_devices( | ||||
| async def test_duplicate_entities_not_allowed_on_different_devices( | ||||
|     yaml_config: str, | ||||
|     run_compiled: RunCompiledFunction, | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test that duplicate entity names are allowed on different devices.""" | ||||
|     """Test that duplicate entity names are NOT allowed on different devices.""" | ||||
|     async with run_compiled(yaml_config), api_client_connected() as client: | ||||
|         # Get device info | ||||
|         device_info = await client.device_info() | ||||
| @@ -53,41 +53,44 @@ async def test_duplicate_entities_on_different_devices( | ||||
|         buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"] | ||||
|         numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"] | ||||
|  | ||||
|         # Scenario 1: Check sensors with same "Temperature" name on different devices | ||||
|         temp_sensors = [s for s in sensors if s.name == "Temperature"] | ||||
|         # Scenario 1: Check that temperature sensors have unique names per device | ||||
|         temp_sensors = [s for s in sensors if "Temperature" in s.name] | ||||
|         assert len(temp_sensors) == 4, ( | ||||
|             f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}" | ||||
|         ) | ||||
|  | ||||
|         # Verify each sensor is on a different device | ||||
|         temp_device_ids = set() | ||||
|         # Verify each sensor has a unique name | ||||
|         temp_names = set() | ||||
|         temp_object_ids = set() | ||||
|  | ||||
|         for sensor in temp_sensors: | ||||
|             temp_device_ids.add(sensor.device_id) | ||||
|             temp_names.add(sensor.name) | ||||
|             temp_object_ids.add(sensor.object_id) | ||||
|  | ||||
|             # All should have object_id "temperature" (no suffix) | ||||
|             assert sensor.object_id == "temperature", ( | ||||
|                 f"Expected object_id 'temperature', got '{sensor.object_id}'" | ||||
|             ) | ||||
|  | ||||
|         # Should have 4 different device IDs (including None for main device) | ||||
|         assert len(temp_device_ids) == 4, ( | ||||
|             f"Temperature sensors should be on different devices, got {temp_device_ids}" | ||||
|         # Should have 4 unique names | ||||
|         assert len(temp_names) == 4, ( | ||||
|             f"Temperature sensors should have unique names, got {temp_names}" | ||||
|         ) | ||||
|  | ||||
|         # Scenario 2: Check binary sensors "Status" on different devices | ||||
|         status_binary = [b for b in binary_sensors if b.name == "Status"] | ||||
|         # Object IDs should also be unique | ||||
|         assert len(temp_object_ids) == 4, ( | ||||
|             f"Temperature sensors should have unique object_ids, got {temp_object_ids}" | ||||
|         ) | ||||
|  | ||||
|         # Scenario 2: Check binary sensors have unique names | ||||
|         status_binary = [b for b in binary_sensors if "Status" in b.name] | ||||
|         assert len(status_binary) == 3, ( | ||||
|             f"Expected exactly 3 status binary sensors, got {len(status_binary)}" | ||||
|         ) | ||||
|  | ||||
|         # All should have object_id "status" | ||||
|         # All should have unique object_ids | ||||
|         status_names = set() | ||||
|         for binary in status_binary: | ||||
|             assert binary.object_id == "status", ( | ||||
|                 f"Expected object_id 'status', got '{binary.object_id}'" | ||||
|             ) | ||||
|             status_names.add(binary.name) | ||||
|  | ||||
|         assert len(status_names) == 3, ( | ||||
|             f"Status binary sensors should have unique names, got {status_names}" | ||||
|         ) | ||||
|  | ||||
|         # Scenario 3: Check that sensor and binary_sensor can have same name | ||||
|         temp_binary = [b for b in binary_sensors if b.name == "Temperature"] | ||||
| @@ -96,62 +99,65 @@ async def test_duplicate_entities_on_different_devices( | ||||
|         ) | ||||
|         assert temp_binary[0].object_id == "temperature" | ||||
|  | ||||
|         # Scenario 4: Check text sensors "Device Info" on different devices | ||||
|         info_text = [t for t in text_sensors if t.name == "Device Info"] | ||||
|         # Scenario 4: Check text sensors have unique names | ||||
|         info_text = [t for t in text_sensors if "Device Info" in t.name] | ||||
|         assert len(info_text) == 3, ( | ||||
|             f"Expected exactly 3 device info text sensors, got {len(info_text)}" | ||||
|         ) | ||||
|  | ||||
|         # All should have object_id "device_info" | ||||
|         # All should have unique names and object_ids | ||||
|         info_names = set() | ||||
|         for text in info_text: | ||||
|             assert text.object_id == "device_info", ( | ||||
|                 f"Expected object_id 'device_info', got '{text.object_id}'" | ||||
|             ) | ||||
|             info_names.add(text.name) | ||||
|  | ||||
|         # Scenario 5: Check switches "Power" on different devices | ||||
|         power_switches = [s for s in switches if s.name == "Power"] | ||||
|         assert len(power_switches) == 3, ( | ||||
|             f"Expected exactly 3 power switches, got {len(power_switches)}" | ||||
|         assert len(info_names) == 3, ( | ||||
|             f"Device info text sensors should have unique names, got {info_names}" | ||||
|         ) | ||||
|  | ||||
|         # All should have object_id "power" | ||||
|         # Scenario 5: Check switches have unique names | ||||
|         power_switches = [s for s in switches if "Power" in s.name] | ||||
|         assert len(power_switches) == 4, ( | ||||
|             f"Expected exactly 4 power switches, got {len(power_switches)}" | ||||
|         ) | ||||
|  | ||||
|         # All should have unique names | ||||
|         power_names = set() | ||||
|         for switch in power_switches: | ||||
|             assert switch.object_id == "power", ( | ||||
|                 f"Expected object_id 'power', got '{switch.object_id}'" | ||||
|             ) | ||||
|             power_names.add(switch.name) | ||||
|  | ||||
|         # Scenario 6: Check empty name buttons (should use device name) | ||||
|         empty_buttons = [b for b in buttons if b.name == ""] | ||||
|         assert len(empty_buttons) == 3, ( | ||||
|             f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}" | ||||
|         assert len(power_names) == 4, ( | ||||
|             f"Power switches should have unique names, got {power_names}" | ||||
|         ) | ||||
|  | ||||
|         # Group by device | ||||
|         c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id] | ||||
|         c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id] | ||||
|  | ||||
|         # For main device, device_id is 0 | ||||
|         main_buttons = [b for b in empty_buttons if b.device_id == 0] | ||||
|  | ||||
|         # Check object IDs for empty name entities | ||||
|         assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1" | ||||
|         assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2" | ||||
|         assert ( | ||||
|             len(main_buttons) == 1 | ||||
|             and main_buttons[0].object_id == "duplicate-entities-test" | ||||
|         # Scenario 6: Check reset buttons have unique names | ||||
|         reset_buttons = [b for b in buttons if "Reset" in b.name] | ||||
|         assert len(reset_buttons) == 3, ( | ||||
|             f"Expected exactly 3 reset buttons, got {len(reset_buttons)}" | ||||
|         ) | ||||
|  | ||||
|         # Scenario 7: Check special characters in number names | ||||
|         temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"] | ||||
|         # All should have unique names | ||||
|         reset_names = set() | ||||
|         for button in reset_buttons: | ||||
|             reset_names.add(button.name) | ||||
|  | ||||
|         assert len(reset_names) == 3, ( | ||||
|             f"Reset buttons should have unique names, got {reset_names}" | ||||
|         ) | ||||
|  | ||||
|         # Scenario 7: Check special characters in number names - now unique | ||||
|         temp_numbers = [n for n in numbers if "Temperature Setpoint!" in n.name] | ||||
|         assert len(temp_numbers) == 2, ( | ||||
|             f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}" | ||||
|         ) | ||||
|  | ||||
|         # Special characters should be sanitized to _ in object_id | ||||
|         # Should have unique names | ||||
|         setpoint_names = set() | ||||
|         for number in temp_numbers: | ||||
|             assert number.object_id == "temperature_setpoint_", ( | ||||
|                 f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'" | ||||
|             ) | ||||
|             setpoint_names.add(number.name) | ||||
|  | ||||
|         assert len(setpoint_names) == 2, ( | ||||
|             f"Temperature setpoint numbers should have unique names, got {setpoint_names}" | ||||
|         ) | ||||
|  | ||||
|         # Verify we can get states for all entities (ensures they're functional) | ||||
|         loop = asyncio.get_running_loop() | ||||
|   | ||||
| @@ -505,13 +505,13 @@ def test_entity_duplicate_validator() -> None: | ||||
|     config1 = {CONF_NAME: "Temperature"} | ||||
|     validated1 = validator(config1) | ||||
|     assert validated1 == config1 | ||||
|     assert ("", "sensor", "temperature") in CORE.unique_ids | ||||
|     assert ("sensor", "temperature") in CORE.unique_ids | ||||
|  | ||||
|     # Second entity with different name should pass | ||||
|     config2 = {CONF_NAME: "Humidity"} | ||||
|     validated2 = validator(config2) | ||||
|     assert validated2 == config2 | ||||
|     assert ("", "sensor", "humidity") in CORE.unique_ids | ||||
|     assert ("sensor", "humidity") in CORE.unique_ids | ||||
|  | ||||
|     # Duplicate entity should fail | ||||
|     config3 = {CONF_NAME: "Temperature"} | ||||
| @@ -535,24 +535,25 @@ def test_entity_duplicate_validator_with_devices() -> None: | ||||
|     device1 = ID("device1", type="Device") | ||||
|     device2 = ID("device2", type="Device") | ||||
|  | ||||
|     # Same name on different devices should pass | ||||
|     # First entity on device1 should pass | ||||
|     config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} | ||||
|     validated1 = validator(config1) | ||||
|     assert validated1 == config1 | ||||
|     assert ("device1", "sensor", "temperature") in CORE.unique_ids | ||||
|     assert ("sensor", "temperature") in CORE.unique_ids | ||||
|  | ||||
|     # Same name on different device should now fail | ||||
|     config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} | ||||
|     validated2 = validator(config2) | ||||
|     assert validated2 == config2 | ||||
|     assert ("device2", "sensor", "temperature") in CORE.unique_ids | ||||
|  | ||||
|     # Duplicate on same device should fail | ||||
|     config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} | ||||
|     with pytest.raises( | ||||
|         Invalid, | ||||
|         match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'", | ||||
|         match=r"Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices.", | ||||
|     ): | ||||
|         validator(config3) | ||||
|         validator(config2) | ||||
|  | ||||
|     # Different name on device2 should pass | ||||
|     config3 = {CONF_NAME: "Humidity", CONF_DEVICE_ID: device2} | ||||
|     validated3 = validator(config3) | ||||
|     assert validated3 == config3 | ||||
|     assert ("sensor", "humidity") in CORE.unique_ids | ||||
|  | ||||
|  | ||||
| def test_duplicate_entity_yaml_validation( | ||||
| @@ -576,10 +577,10 @@ def test_duplicate_entity_with_devices_yaml_validation( | ||||
|     ) | ||||
|     assert result is None | ||||
|  | ||||
|     # Check for the duplicate entity error message with device | ||||
|     # Check for the duplicate entity error message | ||||
|     captured = capsys.readouterr() | ||||
|     assert ( | ||||
|         "Duplicate sensor entity with name 'Temperature' found on device 'device1'" | ||||
|         "Duplicate sensor entity with name 'Temperature' found. Each entity must have a unique name within its platform across all devices." | ||||
|         in captured.out | ||||
|     ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user