mirror of
https://github.com/esphome/esphome.git
synced 2025-08-06 18:37:47 +00:00
Merge branch 'entity_name_must_be_unique' into integration
This commit is contained in:
commit
ae981ea7f2
@ -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
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user