mirror of
https://github.com/esphome/esphome.git
synced 2025-08-10 12:27:46 +00:00
raise
This commit is contained in:
@@ -523,8 +523,8 @@ class EsphomeCore:
|
||||
# Key: platform name (e.g. "sensor", "binary_sensor"), Value: count
|
||||
self.platform_counts: defaultdict[str, int] = defaultdict(int)
|
||||
# Track entity unique IDs to handle duplicates
|
||||
# Key: (device_id, platform, object_id), Value: count of duplicates
|
||||
self.unique_ids: dict[tuple[int, str, str], int] = {}
|
||||
# Set of (device_id, platform, object_id) tuples
|
||||
self.unique_ids: set[tuple[int, str, str]] = set()
|
||||
# Whether ESPHome was started in verbose mode
|
||||
self.verbose = False
|
||||
# Whether ESPHome was started in quiet mode
|
||||
@@ -556,7 +556,7 @@ class EsphomeCore:
|
||||
self.loaded_integrations = set()
|
||||
self.component_ids = set()
|
||||
self.platform_counts = defaultdict(int)
|
||||
self.unique_ids = {}
|
||||
self.unique_ids = set()
|
||||
PIN_SCHEMA_REGISTRY.reset()
|
||||
|
||||
@property
|
||||
|
@@ -99,23 +99,21 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
|
||||
"Entity has empty name, using '%s' as object_id base", base_object_id
|
||||
)
|
||||
|
||||
# Handle duplicates
|
||||
# Check for duplicates
|
||||
unique_key: tuple[int, str, str] = (device_id, platform, base_object_id)
|
||||
if unique_key in CORE.unique_ids:
|
||||
# Found duplicate, add suffix
|
||||
count = CORE.unique_ids[unique_key] + 1
|
||||
CORE.unique_ids[unique_key] = count
|
||||
object_id = f"{base_object_id}_{count}"
|
||||
_LOGGER.info(
|
||||
"Duplicate %s entity '%s' found. Renaming to '%s'",
|
||||
platform,
|
||||
config[CONF_NAME],
|
||||
object_id,
|
||||
# Found duplicate - fail validation
|
||||
from esphome.config_validation import Invalid
|
||||
|
||||
entity_name = config[CONF_NAME] or base_object_id
|
||||
device_prefix = f" on device '{device_name}'" if device_name else ""
|
||||
raise 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."
|
||||
)
|
||||
else:
|
||||
# First occurrence
|
||||
CORE.unique_ids[unique_key] = 1
|
||||
# First occurrence - register it
|
||||
CORE.unique_ids.add(unique_key)
|
||||
object_id = base_object_id
|
||||
|
||||
add(var.set_object_id(object_id))
|
||||
|
@@ -7,6 +7,7 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome import entity
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
@@ -302,8 +303,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) ->
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_with_duplicates(setup_test_environment: list[str]) -> None:
|
||||
"""Test setup_entity with duplicate names."""
|
||||
|
||||
"""Test setup_entity with duplicate names raises validation error."""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
# Create mock entities
|
||||
@@ -315,18 +315,21 @@ async def test_setup_entity_with_duplicates(setup_test_environment: list[str]) -
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
object_ids: list[str] = []
|
||||
for var in entities:
|
||||
added_expressions.clear()
|
||||
await setup_entity(var, config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
object_ids.append(object_id)
|
||||
# First entity should succeed
|
||||
await setup_entity(entities[0], config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
assert object_id == "temperature"
|
||||
|
||||
# Check that object IDs were set with proper suffixes
|
||||
assert object_ids[0] == "temperature"
|
||||
assert object_ids[1] == "temperature_2"
|
||||
assert object_ids[2] == "temperature_3"
|
||||
assert object_ids[3] == "temperature_4"
|
||||
# Clear CORE unique_ids before second test to ensure clean state
|
||||
CORE.unique_ids.clear()
|
||||
# Add back the first one
|
||||
CORE.unique_ids.add((0, "sensor", "temperature"))
|
||||
|
||||
# Second entity with same name should raise Invalid
|
||||
with pytest.raises(
|
||||
Invalid, match=r"Duplicate sensor entity with name 'Temperature' found"
|
||||
):
|
||||
await setup_entity(entities[1], config, "sensor")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -452,8 +455,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non
|
||||
async def test_setup_entity_empty_name_duplicates(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity with multiple empty names."""
|
||||
|
||||
"""Test setup_entity with multiple empty names raises validation error."""
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
entities = [MockObj(f"sensor{i}") for i in range(3)]
|
||||
@@ -463,17 +465,20 @@ async def test_setup_entity_empty_name_duplicates(
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
object_ids: list[str] = []
|
||||
for var in entities:
|
||||
added_expressions.clear()
|
||||
await setup_entity(var, config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
object_ids.append(object_id)
|
||||
# First entity should succeed
|
||||
await setup_entity(entities[0], config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
assert object_id == "test_device"
|
||||
|
||||
# Should use device name with suffixes
|
||||
assert object_ids[0] == "test_device"
|
||||
assert object_ids[1] == "test_device_2"
|
||||
assert object_ids[2] == "test_device_3"
|
||||
# Clear and restore unique_ids for clean test
|
||||
CORE.unique_ids.clear()
|
||||
CORE.unique_ids.add((0, "sensor", "test_device"))
|
||||
|
||||
# Second entity with empty name should raise Invalid
|
||||
with pytest.raises(
|
||||
Invalid, match=r"Duplicate sensor entity with name 'test_device' found"
|
||||
):
|
||||
await setup_entity(entities[1], config, "sensor")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -484,24 +489,18 @@ async def test_setup_entity_special_characters(
|
||||
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
entities = [MockObj(f"sensor{i}") for i in range(3)]
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
CONF_NAME: "Temperature Sensor!",
|
||||
CONF_DISABLED_BY_DEFAULT: False,
|
||||
}
|
||||
|
||||
object_ids: list[str] = []
|
||||
for var in entities:
|
||||
added_expressions.clear()
|
||||
await setup_entity(var, config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
object_ids.append(object_id)
|
||||
await setup_entity(var, config, "sensor")
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
|
||||
# Special characters should be sanitized
|
||||
assert object_ids[0] == "temperature_sensor_"
|
||||
assert object_ids[1] == "temperature_sensor__2"
|
||||
assert object_ids[2] == "temperature_sensor__3"
|
||||
assert object_id == "temperature_sensor_"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -558,27 +557,39 @@ async def test_setup_entity_mixed_duplicates(setup_test_environment: list[str])
|
||||
# Track results
|
||||
results: list[tuple[str, str]] = []
|
||||
|
||||
# 3 sensors named "Status"
|
||||
for i in range(3):
|
||||
added_expressions.clear()
|
||||
var = MockObj(f"sensor_status_{i}")
|
||||
await setup_entity(
|
||||
var, {CONF_NAME: "Status", CONF_DISABLED_BY_DEFAULT: False}, "sensor"
|
||||
)
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
results.append(("sensor", object_id))
|
||||
# First sensor named "Status" should succeed
|
||||
added_expressions.clear()
|
||||
var = MockObj("sensor_status_0")
|
||||
await setup_entity(
|
||||
var, {CONF_NAME: "Status", CONF_DISABLED_BY_DEFAULT: False}, "sensor"
|
||||
)
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
results.append(("sensor", object_id))
|
||||
|
||||
# 2 binary_sensors named "Status"
|
||||
for i in range(2):
|
||||
added_expressions.clear()
|
||||
var = MockObj(f"binary_sensor_status_{i}")
|
||||
await setup_entity(
|
||||
var, {CONF_NAME: "Status", CONF_DISABLED_BY_DEFAULT: False}, "binary_sensor"
|
||||
)
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
results.append(("binary_sensor", object_id))
|
||||
# Clear and restore unique_ids for test
|
||||
CORE.unique_ids.clear()
|
||||
CORE.unique_ids.add((0, "sensor", "status"))
|
||||
|
||||
# 1 text_sensor named "Status"
|
||||
# Second sensor with same name should fail
|
||||
with pytest.raises(
|
||||
Invalid, match=r"Duplicate sensor entity with name 'Status' found"
|
||||
):
|
||||
await setup_entity(
|
||||
MockObj("sensor_status_1"),
|
||||
{CONF_NAME: "Status", CONF_DISABLED_BY_DEFAULT: False},
|
||||
"sensor",
|
||||
)
|
||||
|
||||
# Binary sensor with same name should succeed (different platform)
|
||||
added_expressions.clear()
|
||||
var = MockObj("binary_sensor_status_0")
|
||||
await setup_entity(
|
||||
var, {CONF_NAME: "Status", CONF_DISABLED_BY_DEFAULT: False}, "binary_sensor"
|
||||
)
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
results.append(("binary_sensor", object_id))
|
||||
|
||||
# Text sensor with same name should succeed (different platform)
|
||||
added_expressions.clear()
|
||||
var = MockObj("text_sensor_status")
|
||||
await setup_entity(
|
||||
@@ -589,8 +600,8 @@ async def test_setup_entity_mixed_duplicates(setup_test_environment: list[str])
|
||||
|
||||
# Check results - each platform has its own namespace
|
||||
assert results[0] == ("sensor", "status") # sensor
|
||||
assert results[1] == ("sensor", "status_2") # sensor
|
||||
assert results[2] == ("sensor", "status_3") # sensor
|
||||
assert results[3] == ("binary_sensor", "status") # binary_sensor (new namespace)
|
||||
assert results[4] == ("binary_sensor", "status_2") # binary_sensor
|
||||
assert results[5] == ("text_sensor", "status") # text_sensor (new namespace)
|
||||
assert results[1] == (
|
||||
"binary_sensor",
|
||||
"status",
|
||||
) # binary_sensor (different platform)
|
||||
assert results[2] == ("text_sensor", "status") # text_sensor (different platform)
|
||||
|
Reference in New Issue
Block a user