mirror of
https://github.com/esphome/esphome.git
synced 2025-10-30 14:43:51 +00:00
[core] Improve entity duplicate validation error messages (#10184)
This commit is contained in:
@@ -627,13 +627,15 @@ class SchemaValidationStep(ConfigValidationStep):
|
||||
def __init__(
|
||||
self, domain: str, path: ConfigPath, conf: ConfigType, comp: ComponentManifest
|
||||
):
|
||||
self.domain = domain
|
||||
self.path = path
|
||||
self.conf = conf
|
||||
self.comp = comp
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
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:
|
||||
# Remove 'platform' key for validation
|
||||
input_conf = OrderedDict(self.conf)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
@@ -38,7 +39,7 @@ from esphome.util import OrderedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..cpp_generator import MockObj, MockObjClass, Statement
|
||||
from ..types import ConfigType
|
||||
from ..types import ConfigType, EntityMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -571,14 +572,16 @@ 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
|
||||
# Set of (device_id, platform, sanitized_name) tuples
|
||||
self.unique_ids: set[tuple[str, str, str]] = set()
|
||||
# Dict mapping (device_id, platform, sanitized_name) -> entity metadata
|
||||
self.unique_ids: dict[tuple[str, str, str], EntityMetadata] = {}
|
||||
# Whether ESPHome was started in verbose mode
|
||||
self.verbose = False
|
||||
# Whether ESPHome was started in quiet mode
|
||||
self.quiet = False
|
||||
# A list of all known ID classes
|
||||
self.id_classes = {}
|
||||
# The current component being processed during validation
|
||||
self.current_component: str | None = None
|
||||
|
||||
def reset(self):
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
@@ -604,9 +607,20 @@ class EsphomeCore:
|
||||
self.loaded_integrations = set()
|
||||
self.component_ids = set()
|
||||
self.platform_counts = defaultdict(int)
|
||||
self.unique_ids = set()
|
||||
self.unique_ids = {}
|
||||
self.current_component = None
|
||||
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
|
||||
def address(self) -> str | 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
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import sanitize, snake_case
|
||||
from esphome.types import ConfigType
|
||||
from esphome.types import ConfigType, EntityMetadata
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -214,14 +214,45 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
||||
# Check for duplicates
|
||||
unique_key = (device_id, platform, name_key)
|
||||
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 ""
|
||||
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(
|
||||
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."
|
||||
)
|
||||
|
||||
# Add to tracking set
|
||||
CORE.unique_ids.add(unique_key)
|
||||
# Store metadata about this entity
|
||||
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 validator
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""This helper module tracks commonly used types in the esphome python codebase."""
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from esphome.core import ID, EsphomeCore, Lambda
|
||||
|
||||
ConfigFragmentType = (
|
||||
@@ -16,3 +18,13 @@ ConfigFragmentType = (
|
||||
ConfigType = dict[str, ConfigFragmentType]
|
||||
CoreType = EsphomeCore
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user