From 7dbec6abf7fea65e411c19dbf683f939e6b7a435 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Oct 2026 15:51:05 -1000 Subject: [PATCH] [ld6002b] Check each area_config setting on its own for duplicates, and test the check --- esphome/components/ld6002b/__init__.py | 26 +++++--- .../ld6002b/test_final_validate.py | 61 ++++++++++++++++++- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/esphome/components/ld6002b/__init__.py b/esphome/components/ld6002b/__init__.py index 9663f3802a7..860f146bfa4 100644 --- a/esphome/components/ld6002b/__init__.py +++ b/esphome/components/ld6002b/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_ID, CONF_PLATFORM, CONF_WAKEUP_PIN import esphome.final_validate as fv from esphome.types import ConfigType -from .const import CONF_AUTO_WAKE, CONF_LD6002B_ID, CONF_WAKEUP_PULSE +from .const import CONF_AREA_CONFIG, CONF_AUTO_WAKE, CONF_LD6002B_ID, CONF_WAKEUP_PULSE CODEOWNERS = ["@hepter"] DEPENDENCIES = ["uart"] @@ -84,11 +84,23 @@ def validate_unique_settings(domain: str, config: ConfigType) -> None: for entry in fv.full_config.get().get(domain, []) if entry.get(CONF_PLATFORM) == DOMAIN and entry.get(CONF_LD6002B_ID) == hub_id ] - for key in config: + for key, value in config.items(): if key in (CONF_PLATFORM, CONF_LD6002B_ID): continue - if sum(key in entry for entry in entries) > 1: - raise cv.Invalid( - f"{key} is already set for this ld6002b in another {domain} block", - path=[key], - ) + # area_config groups six settings, so each one is checked on its own + for path in ( + [[key, sub] for sub in value] if key == CONF_AREA_CONFIG else [[key]] + ): + if sum(_has_path(entry, path) for entry in entries) > 1: + raise cv.Invalid( + f"{'.'.join(path)} is already set for this ld6002b in another {domain} block", + path=path, + ) + + +def _has_path(entry: ConfigType, path: list[str]) -> bool: + for key in path: + if key not in entry: + return False + entry = entry[key] + return True diff --git a/tests/component_tests/ld6002b/test_final_validate.py b/tests/component_tests/ld6002b/test_final_validate.py index 0bb091533b4..5a666c7f186 100644 --- a/tests/component_tests/ld6002b/test_final_validate.py +++ b/tests/component_tests/ld6002b/test_final_validate.py @@ -9,11 +9,17 @@ from __future__ import annotations import pytest +from esphome.components.ld6002b import validate_unique_settings from esphome.components.ld6002b.button import ( CONFIG_SCHEMA as BUTTON_CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA as BUTTON_FINAL_VALIDATE_SCHEMA, ) -from esphome.components.ld6002b.const import CONF_AREA_CONFIG, CONF_Z_MIN +from esphome.components.ld6002b.const import ( + CONF_AREA_CONFIG, + CONF_HOLD_DELAY, + CONF_Z_MAX, + CONF_Z_MIN, +) from esphome.components.ld6002b.number import ( CONFIG_SCHEMA as NUMBER_CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA as NUMBER_FINAL_VALIDATE_SCHEMA, @@ -40,6 +46,7 @@ def _full_config( *, selects: list[ConfigType] | None = None, buttons: list[ConfigType] | None = None, + numbers: list[ConfigType] | None = None, ) -> Config: """A full config carrying one ld6002b hub, as the ID pass leaves it. @@ -57,6 +64,8 @@ def _full_config( full["select"] = selects if buttons is not None: full[CONF_BUTTON] = buttons + if numbers is not None: + full["number"] = numbers return full @@ -214,3 +223,53 @@ def test_area_config_without_select_is_rejected( ), ): _validated_numbers(_area_numbers()) + + +def _number_block(**settings: ConfigType) -> ConfigType: + """A number platform block on the shared hub, as final validation sees it.""" + return { + "platform": "ld6002b", + "ld6002b_id": ID(HUB_ID, is_declaration=False, type="ld6002b"), + **settings, + } + + +@pytest.mark.parametrize( + ("first", "second", "error"), + [ + ( + {CONF_HOLD_DELAY: {"name": "A"}}, + {CONF_HOLD_DELAY: {"name": "B"}}, + r"^hold_delay is already set .* @ data\['hold_delay'\]$", + ), + ( + {CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "A"}}}, + {CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "B"}}}, + r"^area_config\.z_min is already set .* @ data\['area_config'\]\['z_min'\]$", + ), + ( + {CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "A"}}}, + {CONF_AREA_CONFIG: {CONF_Z_MAX: {"name": "B"}}}, + None, + ), + ({CONF_HOLD_DELAY: {"name": "A"}}, {CONF_Z_MIN: {"name": "B"}}, None), + ], +) +def test_setting_in_two_blocks_of_one_hub( + set_core_config: SetCoreConfigCallable, + first: ConfigType, + second: ConfigType, + error: str | None, +) -> None: + """Each setting maps to one entity pointer, so two blocks cannot share one.""" + blocks = [_number_block(**first), _number_block(**second)] + set_core_config( + PlatformFramework.ESP32_IDF, + full_config=_full_config(_hub(wakeup_pin=False), numbers=blocks), + ) + + if error is None: + validate_unique_settings("number", blocks[1]) + return + with pytest.raises(cv.Invalid, match=error): + validate_unique_settings("number", blocks[1])