Files
esphome/tests/component_tests/ld6002b/test_final_validate.py
T

278 lines
8.6 KiB
Python

"""Tests for the ld6002b validators that reach across platforms.
wake needs a pin on its own hub, apply_area needs a select on its own hub, and
area_config needs both a button and a select on its own hub. Every one of them
is a same-instance check, which is the half that breaks quietly.
"""
from __future__ import annotations
import pytest
from esphome.components.ld600x.const import (
CONF_AREA_CONFIG,
CONF_HOLD_DELAY,
CONF_Z_MAX,
CONF_Z_MIN,
)
from esphome.components.ld600x.entities import unique_settings_final_validate
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_LD6002B_ID
from esphome.components.ld6002b.number import (
CONFIG_SCHEMA as NUMBER_CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA as NUMBER_FINAL_VALIDATE_SCHEMA,
)
from esphome.config import Config
import esphome.config_validation as cv
from esphome.const import (
CONF_AREA_ID,
CONF_BUTTON,
CONF_ID,
CONF_WAKEUP_PIN,
PlatformFramework,
)
from esphome.core import ID
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
HUB_ID = "ld6002b_hub"
OTHER_HUB_ID = "ld6002b_other"
def _full_config(
hub: ConfigType,
*,
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.
final_validate resolves the hub through get_path_for_id, so the declaring
path has to be registered the way validate_config registers it: the path of
the id value itself, whose parent is the hub's own config.
The platform lists are what the cross-platform validators scan, so a test can
say which of them exist and which hub each one names.
"""
full = Config()
full["ld6002b"] = [hub]
full.declare_ids.append((hub[CONF_ID], ["ld6002b", 0, CONF_ID]))
if selects is not None:
full["select"] = selects
if buttons is not None:
full[CONF_BUTTON] = buttons
if numbers is not None:
full["number"] = numbers
return full
def _hub(*, wakeup_pin: bool) -> ConfigType:
hub: ConfigType = {CONF_ID: ID(HUB_ID, is_declaration=True, type="ld6002b")}
if wakeup_pin:
hub[CONF_WAKEUP_PIN] = {"number": 4}
return hub
def _buttons(**buttons: str) -> ConfigType:
"""A button platform config naming the given buttons on the shared hub."""
config: ConfigType = {
"ld6002b_id": ID(HUB_ID, is_declaration=False, type="ld6002b")
}
config.update({key: {"name": name} for key, name in buttons.items()})
return config
def _select(*, hub_id: str = HUB_ID) -> ConfigType:
"""A select platform config naming area_id on the given hub."""
return {
"ld6002b_id": ID(hub_id, is_declaration=False, type="ld6002b"),
CONF_AREA_ID: {"name": "Area ID"},
}
def _area_numbers(*, hub_id: str = HUB_ID) -> ConfigType:
"""A number platform config carrying one area_config bound."""
return {
"ld6002b_id": ID(hub_id, is_declaration=False, type="ld6002b"),
CONF_AREA_CONFIG: {CONF_Z_MIN: {"name": "Area Z Min"}},
}
def _validated(config: ConfigType) -> ConfigType:
"""Run the button schema, then the final validation the hub is checked in."""
config = BUTTON_CONFIG_SCHEMA(config)
BUTTON_FINAL_VALIDATE_SCHEMA(config)
return config
def _validated_numbers(config: ConfigType) -> ConfigType:
"""The same two passes for the number platform."""
config = NUMBER_CONFIG_SCHEMA(config)
NUMBER_FINAL_VALIDATE_SCHEMA(config)
return config
def test_wake_without_wakeup_pin_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The wake button drives the pin directly, so a hub without one cannot serve it."""
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False))
)
with pytest.raises(cv.Invalid, match="wake requires wakeup_pin"):
_validated(_buttons(wake="Wake"))
def test_wake_with_wakeup_pin_passes(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=True))
)
_validated(_buttons(wake="Wake"))
def test_other_buttons_do_not_need_the_pin(
set_core_config: SetCoreConfigCallable,
) -> None:
"""Only wake drives the pin; the query buttons stay usable without one."""
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False))
)
_validated(_buttons(get_delay="Get Delay"))
def test_apply_area_without_select_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""apply_area sends the staged bounds to whichever area the select names."""
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_hub(wakeup_pin=False))
)
with pytest.raises(
cv.Invalid,
match=(
r"^apply_area requires select\.area_id for the same ld6002b instance"
r" @ data\['apply_area'\]$"
),
):
_validated(_buttons(apply_area="Apply Area"))
def test_apply_area_select_on_another_hub_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A select exists, but on a second ld6002b -- which cannot serve this one."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config(
_hub(wakeup_pin=False), selects=[_select(hub_id=OTHER_HUB_ID)]
),
)
with pytest.raises(
cv.Invalid,
match=(
r"^apply_area requires select\.area_id for the same ld6002b instance"
r" @ data\['apply_area'\]$"
),
):
_validated(_buttons(apply_area="Apply Area"))
def test_area_config_without_apply_area_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The six numbers only stage a write; apply_area is what sends it."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config(_hub(wakeup_pin=False), selects=[_select()]),
)
with pytest.raises(
cv.Invalid,
match=(
r"^area_config requires button\.apply_area for the same ld6002b instance"
r" @ data\['area_config'\]$"
),
):
_validated_numbers(_area_numbers())
def test_area_config_without_select_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The validator's other half: the staged bounds also need an area to land in."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config(
_hub(wakeup_pin=False), buttons=[_buttons(apply_area="Apply Area")]
),
)
with pytest.raises(
cv.Invalid,
match=(
r"^area_config requires select\.area_id for the same ld6002b instance"
r" @ data\['area_config'\]$"
),
):
_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),
)
validate = unique_settings_final_validate("number", CONF_LD6002B_ID, "ld6002b")
if error is None:
validate(blocks[1])
return
with pytest.raises(cv.Invalid, match=error):
validate(blocks[1])