mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'improve_entity_validation_message' into integration
This commit is contained in:
		| @@ -596,7 +596,7 @@ async def throttle_filter_to_code(config, filter_id): | |||||||
|     return cg.new_Pvariable(filter_id, config) |     return cg.new_Pvariable(filter_id, config) | ||||||
|  |  | ||||||
|  |  | ||||||
| TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( | THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( | ||||||
|     { |     { | ||||||
|         cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, |         cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, | ||||||
|         cv.Optional(CONF_VALUE, default="nan"): cv.Any( |         cv.Optional(CONF_VALUE, default="nan"): cv.Any( | ||||||
| @@ -610,7 +610,7 @@ TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value( | |||||||
| @FILTER_REGISTRY.register( | @FILTER_REGISTRY.register( | ||||||
|     "throttle_with_priority", |     "throttle_with_priority", | ||||||
|     ThrottleWithPriorityFilter, |     ThrottleWithPriorityFilter, | ||||||
|     TIMEOUT_WITH_PRIORITY_SCHEMA, |     THROTTLE_WITH_PRIORITY_SCHEMA, | ||||||
| ) | ) | ||||||
| async def throttle_with_priority_filter_to_code(config, filter_id): | async def throttle_with_priority_filter_to_code(config, filter_id): | ||||||
|     if not isinstance(config[CONF_VALUE], list): |     if not isinstance(config[CONF_VALUE], list): | ||||||
| @@ -631,7 +631,9 @@ async def heartbeat_filter_to_code(config, filter_id): | |||||||
| TIMEOUT_SCHEMA = cv.maybe_simple_value( | TIMEOUT_SCHEMA = cv.maybe_simple_value( | ||||||
|     { |     { | ||||||
|         cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, |         cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, | ||||||
|         cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_), |         cv.Optional(CONF_VALUE, default="nan"): cv.Any( | ||||||
|  |             "last", cv.templatable(cv.float_) | ||||||
|  |         ), | ||||||
|     }, |     }, | ||||||
|     key=CONF_TIMEOUT, |     key=CONF_TIMEOUT, | ||||||
| ) | ) | ||||||
| @@ -639,6 +641,9 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( | |||||||
|  |  | ||||||
| @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) | @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) | ||||||
| async def timeout_filter_to_code(config, filter_id): | async def timeout_filter_to_code(config, filter_id): | ||||||
|  |     if config[CONF_VALUE] == "last": | ||||||
|  |         var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT]) | ||||||
|  |     else: | ||||||
|         template_ = await cg.templatable(config[CONF_VALUE], [], float) |         template_ = await cg.templatable(config[CONF_VALUE], [], float) | ||||||
|         var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) |         var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) | ||||||
|     await cg.register_component(var, {}) |     await cg.register_component(var, {}) | ||||||
|   | |||||||
| @@ -417,12 +417,17 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { | |||||||
|  |  | ||||||
| // TimeoutFilter | // TimeoutFilter | ||||||
| optional<float> TimeoutFilter::new_value(float value) { | optional<float> TimeoutFilter::new_value(float value) { | ||||||
|   this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); }); |   if (this->value_.has_value()) { | ||||||
|  |     this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value().value()); }); | ||||||
|  |   } else { | ||||||
|  |     this->set_timeout("timeout", this->time_period_, [this, value]() { this->output(value); }); | ||||||
|  |   } | ||||||
|   return value; |   return value; | ||||||
| } | } | ||||||
|  |  | ||||||
| TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue<float> new_value) | TimeoutFilter::TimeoutFilter(uint32_t time_period) : time_period_(time_period) {} | ||||||
|     : time_period_(time_period), value_(std::move(new_value)) {} | TimeoutFilter::TimeoutFilter(uint32_t time_period, const TemplatableValue<float> &new_value) | ||||||
|  |     : time_period_(time_period), value_(new_value) {} | ||||||
| float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } | float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
| // DebounceFilter | // DebounceFilter | ||||||
|   | |||||||
| @@ -330,7 +330,8 @@ class ThrottleWithPriorityFilter : public Filter { | |||||||
|  |  | ||||||
| class TimeoutFilter : public Filter, public Component { | class TimeoutFilter : public Filter, public Component { | ||||||
|  public: |  public: | ||||||
|   explicit TimeoutFilter(uint32_t time_period, TemplatableValue<float> new_value); |   explicit TimeoutFilter(uint32_t time_period); | ||||||
|  |   explicit TimeoutFilter(uint32_t time_period, const TemplatableValue<float> &new_value); | ||||||
|  |  | ||||||
|   optional<float> new_value(float value) override; |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
| @@ -338,7 +339,7 @@ class TimeoutFilter : public Filter, public Component { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   uint32_t time_period_; |   uint32_t time_period_; | ||||||
|   TemplatableValue<float> value_; |   optional<TemplatableValue<float>> value_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class DebounceFilter : public Filter, public Component { | class DebounceFilter : public Filter, public Component { | ||||||
|   | |||||||
| @@ -627,13 +627,15 @@ class SchemaValidationStep(ConfigValidationStep): | |||||||
|     def __init__( |     def __init__( | ||||||
|         self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest |         self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest | ||||||
|     ): |     ): | ||||||
|  |         self.domain = domain | ||||||
|         self.path = path |         self.path = path | ||||||
|         self.conf = conf |         self.conf = conf | ||||||
|         self.comp = comp |         self.comp = comp | ||||||
|  |  | ||||||
|     def run(self, result: Config) -> None: |     def run(self, result: Config) -> None: | ||||||
|         token = path_context.set(self.path) |         token = path_context.set(self.path) | ||||||
|         with result.catch_error(self.path): |         # The domain already contains the full component path (e.g., "sensor.template", "sensor.uptime") | ||||||
|  |         with CORE.component_context(self.domain), result.catch_error(self.path): | ||||||
|             if self.comp.is_platform: |             if self.comp.is_platform: | ||||||
|                 # Remove 'platform' key for validation |                 # Remove 'platform' key for validation | ||||||
|                 input_conf = OrderedDict(self.conf) |                 input_conf = OrderedDict(self.conf) | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| from collections import defaultdict | from collections import defaultdict | ||||||
|  | from contextlib import contextmanager | ||||||
| import logging | import logging | ||||||
| import math | import math | ||||||
| import os | import os | ||||||
| @@ -38,7 +39,7 @@ from esphome.util import OrderedDict | |||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ..cpp_generator import MockObj, MockObjClass, Statement |     from ..cpp_generator import MockObj, MockObjClass, Statement | ||||||
|     from ..types import ConfigType |     from ..types import ConfigType, EntityMetadata | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -571,14 +572,16 @@ class EsphomeCore: | |||||||
|         # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count |         # Key: platform name (e.g. "sensor", "binary_sensor"), Value: count | ||||||
|         self.platform_counts: defaultdict[str, int] = defaultdict(int) |         self.platform_counts: defaultdict[str, int] = defaultdict(int) | ||||||
|         # Track entity unique IDs to handle duplicates |         # Track entity unique IDs to handle duplicates | ||||||
|         # Set of (device_id, platform, sanitized_name) tuples |         # Dict mapping (device_id, platform, sanitized_name) -> entity metadata | ||||||
|         self.unique_ids: set[tuple[str, str, str]] = set() |         self.unique_ids: dict[tuple[str, str, str], EntityMetadata] = {} | ||||||
|         # Whether ESPHome was started in verbose mode |         # Whether ESPHome was started in verbose mode | ||||||
|         self.verbose = False |         self.verbose = False | ||||||
|         # Whether ESPHome was started in quiet mode |         # Whether ESPHome was started in quiet mode | ||||||
|         self.quiet = False |         self.quiet = False | ||||||
|         # A list of all known ID classes |         # A list of all known ID classes | ||||||
|         self.id_classes = {} |         self.id_classes = {} | ||||||
|  |         # The current component being processed during validation | ||||||
|  |         self.current_component: str | None = None | ||||||
|  |  | ||||||
|     def reset(self): |     def reset(self): | ||||||
|         from esphome.pins import PIN_SCHEMA_REGISTRY |         from esphome.pins import PIN_SCHEMA_REGISTRY | ||||||
| @@ -604,9 +607,20 @@ class EsphomeCore: | |||||||
|         self.loaded_integrations = set() |         self.loaded_integrations = set() | ||||||
|         self.component_ids = set() |         self.component_ids = set() | ||||||
|         self.platform_counts = defaultdict(int) |         self.platform_counts = defaultdict(int) | ||||||
|         self.unique_ids = set() |         self.unique_ids = {} | ||||||
|  |         self.current_component = None | ||||||
|         PIN_SCHEMA_REGISTRY.reset() |         PIN_SCHEMA_REGISTRY.reset() | ||||||
|  |  | ||||||
|  |     @contextmanager | ||||||
|  |     def component_context(self, component: str): | ||||||
|  |         """Context manager to set the current component being processed.""" | ||||||
|  |         old_component = self.current_component | ||||||
|  |         self.current_component = component | ||||||
|  |         try: | ||||||
|  |             yield | ||||||
|  |         finally: | ||||||
|  |             self.current_component = old_component | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def address(self) -> str | None: |     def address(self) -> str | None: | ||||||
|         if self.config is None: |         if self.config is None: | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ from esphome.core import CORE, ID | |||||||
| from esphome.cpp_generator import MockObj, add, get_variable | from esphome.cpp_generator import MockObj, add, get_variable | ||||||
| import esphome.final_validate as fv | import esphome.final_validate as fv | ||||||
| from esphome.helpers import sanitize, snake_case | from esphome.helpers import sanitize, snake_case | ||||||
| from esphome.types import ConfigType | from esphome.types import ConfigType, EntityMetadata | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -214,14 +214,45 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy | |||||||
|         # Check for duplicates |         # Check for duplicates | ||||||
|         unique_key = (device_id, platform, name_key) |         unique_key = (device_id, platform, name_key) | ||||||
|         if unique_key in CORE.unique_ids: |         if unique_key in CORE.unique_ids: | ||||||
|  |             # Get the existing entity metadata | ||||||
|  |             existing = CORE.unique_ids[unique_key] | ||||||
|  |             existing_name = existing.get("name", entity_name) | ||||||
|  |             existing_device = existing.get("device_id", "") | ||||||
|  |             existing_id = existing.get("entity_id", "unknown") | ||||||
|  |  | ||||||
|  |             # Build detailed error message | ||||||
|             device_prefix = f" on device '{device_id}'" if device_id else "" |             device_prefix = f" on device '{device_id}'" if device_id else "" | ||||||
|  |             existing_device_prefix = ( | ||||||
|  |                 f" on device '{existing_device}'" if existing_device else "" | ||||||
|  |             ) | ||||||
|  |             existing_component = existing.get("component", "unknown") | ||||||
|  |  | ||||||
|  |             # Provide more context about where the duplicate was found | ||||||
|  |             conflict_msg = ( | ||||||
|  |                 f"Conflicts with entity '{existing_name}'{existing_device_prefix}" | ||||||
|  |             ) | ||||||
|  |             if existing_id != "unknown": | ||||||
|  |                 conflict_msg += f" (id: {existing_id})" | ||||||
|  |             if existing_component != "unknown": | ||||||
|  |                 conflict_msg += f" from component '{existing_component}'" | ||||||
|  |  | ||||||
|             raise cv.Invalid( |             raise cv.Invalid( | ||||||
|                 f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " |                 f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " | ||||||
|  |                 f"{conflict_msg}. " | ||||||
|                 f"Each entity on a device must have a unique name within its platform." |                 f"Each entity on a device must have a unique name within its platform." | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Add to tracking set |         # Store metadata about this entity | ||||||
|         CORE.unique_ids.add(unique_key) |         entity_metadata: EntityMetadata = { | ||||||
|  |             "name": entity_name, | ||||||
|  |             "device_id": device_id, | ||||||
|  |             "platform": platform, | ||||||
|  |             "entity_id": str(config.get(CONF_ID, "unknown")), | ||||||
|  |             "component": CORE.current_component or "unknown", | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         # Add to tracking dict | ||||||
|  |         CORE.unique_ids[unique_key] = entity_metadata | ||||||
|         return config |         return config | ||||||
|  |  | ||||||
|     return validator |     return validator | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| """This helper module tracks commonly used types in the esphome python codebase.""" | """This helper module tracks commonly used types in the esphome python codebase.""" | ||||||
|  |  | ||||||
|  | from typing import TypedDict | ||||||
|  |  | ||||||
| from esphome.core import ID, EsphomeCore, Lambda | from esphome.core import ID, EsphomeCore, Lambda | ||||||
|  |  | ||||||
| ConfigFragmentType = ( | ConfigFragmentType = ( | ||||||
| @@ -16,3 +18,13 @@ ConfigFragmentType = ( | |||||||
| ConfigType = dict[str, ConfigFragmentType] | ConfigType = dict[str, ConfigFragmentType] | ||||||
| CoreType = EsphomeCore | CoreType = EsphomeCore | ||||||
| ConfigPathType = str | int | ConfigPathType = str | int | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EntityMetadata(TypedDict): | ||||||
|  |     """Metadata stored for each entity to help with duplicate detection.""" | ||||||
|  |  | ||||||
|  |     name: str | ||||||
|  |     device_id: str | ||||||
|  |     platform: str | ||||||
|  |     entity_id: str | ||||||
|  |     component: str | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| pylint==3.3.7 | pylint==3.3.8 | ||||||
| flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | ||||||
| ruff==0.12.8  # also change in .pre-commit-config.yaml when updating | ruff==0.12.8  # also change in .pre-commit-config.yaml when updating | ||||||
| pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating | pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating | ||||||
|   | |||||||
| @@ -153,6 +153,9 @@ sensor: | |||||||
|       - timeout: |       - timeout: | ||||||
|           timeout: 1h |           timeout: 1h | ||||||
|           value: 20.0 |           value: 20.0 | ||||||
|  |       - timeout: | ||||||
|  |           timeout: 1min | ||||||
|  |           value: last | ||||||
|       - timeout: |       - timeout: | ||||||
|           timeout: 1d |           timeout: 1d | ||||||
|       - to_ntc_resistance: |       - to_ntc_resistance: | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ from esphome.const import ( | |||||||
|     CONF_DEVICE_ID, |     CONF_DEVICE_ID, | ||||||
|     CONF_DISABLED_BY_DEFAULT, |     CONF_DISABLED_BY_DEFAULT, | ||||||
|     CONF_ICON, |     CONF_ICON, | ||||||
|  |     CONF_ID, | ||||||
|     CONF_INTERNAL, |     CONF_INTERNAL, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
| ) | ) | ||||||
| @@ -511,12 +512,18 @@ def test_entity_duplicate_validator() -> None: | |||||||
|     validated1 = validator(config1) |     validated1 = validator(config1) | ||||||
|     assert validated1 == config1 |     assert validated1 == config1 | ||||||
|     assert ("", "sensor", "temperature") in CORE.unique_ids |     assert ("", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |     # Check metadata was stored | ||||||
|  |     metadata = CORE.unique_ids[("", "sensor", "temperature")] | ||||||
|  |     assert metadata["name"] == "Temperature" | ||||||
|  |     assert metadata["platform"] == "sensor" | ||||||
|  |  | ||||||
|     # Second entity with different name should pass |     # Second entity with different name should pass | ||||||
|     config2 = {CONF_NAME: "Humidity"} |     config2 = {CONF_NAME: "Humidity"} | ||||||
|     validated2 = validator(config2) |     validated2 = validator(config2) | ||||||
|     assert validated2 == config2 |     assert validated2 == config2 | ||||||
|     assert ("", "sensor", "humidity") in CORE.unique_ids |     assert ("", "sensor", "humidity") in CORE.unique_ids | ||||||
|  |     metadata2 = CORE.unique_ids[("", "sensor", "humidity")] | ||||||
|  |     assert metadata2["name"] == "Humidity" | ||||||
|  |  | ||||||
|     # Duplicate entity should fail |     # Duplicate entity should fail | ||||||
|     config3 = {CONF_NAME: "Temperature"} |     config3 = {CONF_NAME: "Temperature"} | ||||||
| @@ -540,11 +547,15 @@ def test_entity_duplicate_validator_with_devices() -> None: | |||||||
|     validated1 = validator(config1) |     validated1 = validator(config1) | ||||||
|     assert validated1 == config1 |     assert validated1 == config1 | ||||||
|     assert ("device1", "sensor", "temperature") in CORE.unique_ids |     assert ("device1", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |     metadata1 = CORE.unique_ids[("device1", "sensor", "temperature")] | ||||||
|  |     assert metadata1["device_id"] == "device1" | ||||||
|  |  | ||||||
|     config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} |     config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2} | ||||||
|     validated2 = validator(config2) |     validated2 = validator(config2) | ||||||
|     assert validated2 == config2 |     assert validated2 == config2 | ||||||
|     assert ("device2", "sensor", "temperature") in CORE.unique_ids |     assert ("device2", "sensor", "temperature") in CORE.unique_ids | ||||||
|  |     metadata2 = CORE.unique_ids[("device2", "sensor", "temperature")] | ||||||
|  |     assert metadata2["device_id"] == "device2" | ||||||
|  |  | ||||||
|     # Duplicate on same device should fail |     # Duplicate on same device should fail | ||||||
|     config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} |     config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1} | ||||||
| @@ -595,6 +606,54 @@ def test_entity_different_platforms_yaml_validation( | |||||||
|     assert result is not None |     assert result is not None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_entity_duplicate_validator_error_message() -> None: | ||||||
|  |     """Test that duplicate entity error messages include helpful metadata.""" | ||||||
|  |     # Create validator for sensor platform | ||||||
|  |     validator = entity_duplicate_validator("sensor") | ||||||
|  |  | ||||||
|  |     # Set current component to simulate validation context for uptime sensor | ||||||
|  |     CORE.current_component = "sensor.uptime" | ||||||
|  |  | ||||||
|  |     # First entity should pass | ||||||
|  |     config1 = {CONF_NAME: "Battery", CONF_ID: ID("battery_1")} | ||||||
|  |     validated1 = validator(config1) | ||||||
|  |     assert validated1 == config1 | ||||||
|  |  | ||||||
|  |     # Reset component to simulate template sensor | ||||||
|  |     CORE.current_component = "sensor.template" | ||||||
|  |  | ||||||
|  |     # Duplicate entity should fail with detailed error | ||||||
|  |     config2 = {CONF_NAME: "Battery", CONF_ID: ID("battery_2")} | ||||||
|  |     with pytest.raises( | ||||||
|  |         Invalid, | ||||||
|  |         match=r"Duplicate sensor entity with name 'Battery' found.*" | ||||||
|  |         r"Conflicts with entity 'Battery' \(id: battery_1\) from component 'sensor\.uptime'", | ||||||
|  |     ): | ||||||
|  |         validator(config2) | ||||||
|  |  | ||||||
|  |     # Clean up | ||||||
|  |     CORE.current_component = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_entity_conflict_between_components_yaml( | ||||||
|  |     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||||
|  | ) -> None: | ||||||
|  |     """Test that conflicts between different components show helpful error messages.""" | ||||||
|  |     result = load_config_from_fixture( | ||||||
|  |         yaml_file, "entity_conflict_components.yaml", FIXTURES_DIR | ||||||
|  |     ) | ||||||
|  |     assert result is None | ||||||
|  |  | ||||||
|  |     # Check for the enhanced error message | ||||||
|  |     captured = capsys.readouterr() | ||||||
|  |     # The error should mention both the conflict and which component created it | ||||||
|  |     assert "Duplicate sensor entity with name 'Battery' found" in captured.out | ||||||
|  |     # Should mention it conflicts with an entity from a specific sensor platform | ||||||
|  |     assert "from component 'sensor." in captured.out | ||||||
|  |     # Should show it's a conflict between wifi_signal and template | ||||||
|  |     assert "sensor.wifi_signal" in captured.out or "sensor.template" in captured.out | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_entity_duplicate_validator_internal_entities() -> None: | def test_entity_duplicate_validator_internal_entities() -> None: | ||||||
|     """Test that internal entities are excluded from duplicate name validation.""" |     """Test that internal entities are excluded from duplicate name validation.""" | ||||||
|     # Create validator for sensor platform |     # Create validator for sensor platform | ||||||
| @@ -612,14 +671,17 @@ def test_entity_duplicate_validator_internal_entities() -> None: | |||||||
|     validated2 = validator(config2) |     validated2 = validator(config2) | ||||||
|     assert validated2 == config2 |     assert validated2 == config2 | ||||||
|     # Internal entity should not be added to unique_ids |     # Internal entity should not be added to unique_ids | ||||||
|     assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 |     # Count how many times the key appears (should still be 1) | ||||||
|  |     count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) | ||||||
|  |     assert count == 1 | ||||||
|  |  | ||||||
|     # Another internal entity with same name should also pass |     # Another internal entity with same name should also pass | ||||||
|     config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} |     config3 = {CONF_NAME: "Temperature", CONF_INTERNAL: True} | ||||||
|     validated3 = validator(config3) |     validated3 = validator(config3) | ||||||
|     assert validated3 == config3 |     assert validated3 == config3 | ||||||
|     # Still only one entry in unique_ids (from the non-internal entity) |     # Still only one entry in unique_ids (from the non-internal entity) | ||||||
|     assert len([k for k in CORE.unique_ids if k == ("", "sensor", "temperature")]) == 1 |     count = sum(1 for k in CORE.unique_ids if k == ("", "sensor", "temperature")) | ||||||
|  |     assert count == 1 | ||||||
|  |  | ||||||
|     # Non-internal entity with same name should fail |     # Non-internal entity with same name should fail | ||||||
|     config4 = {CONF_NAME: "Temperature"} |     config4 = {CONF_NAME: "Temperature"} | ||||||
|   | |||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | esphome: | ||||||
|  |   name: test-device | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: esp32dev | ||||||
|  |  | ||||||
|  | # Uptime sensor | ||||||
|  | sensor: | ||||||
|  |   - platform: uptime | ||||||
|  |     name: "Battery" | ||||||
|  |     id: uptime_battery | ||||||
|  |  | ||||||
|  | # Template sensor also named "Battery" - this should conflict | ||||||
|  |   - platform: template | ||||||
|  |     name: "Battery" | ||||||
|  |     id: template_battery | ||||||
|  |     lambda: |- | ||||||
|  |       return 95.0; | ||||||
|  |     unit_of_measurement: "%" | ||||||
|  |     update_interval: 60s | ||||||
		Reference in New Issue
	
	Block a user