Migrate compound conditions to ConditionChecker

This commit is contained in:
Erik
2026-04-23 17:15:39 +02:00
parent 65a68c138c
commit 5a10e105a8
2 changed files with 153 additions and 30 deletions
+56 -30
View File
@@ -358,6 +358,21 @@ class DisabledConditionChecker(ConditionChecker):
return None
class CompoundConditionChecker(ConditionChecker):
"""Base class for compound condition checkers (and/or/not)."""
def __init__(self, hass: HomeAssistant, checks: list[ConditionChecker]) -> None:
"""Initialize condition checker."""
super().__init__(hass)
self._checks = checks
def async_unload(self) -> None:
"""Clean up child conditions."""
for check in self._checks:
check.async_unload()
super().async_unload()
class Condition(ConditionChecker):
"""Condition class."""
@@ -975,31 +990,38 @@ async def async_from_config(
check_factory = check_factory.func
if inspect.iscoroutinefunction(check_factory):
checker = cast(ConditionCheckerType, await factory(hass, config))
checker = await factory(hass, config)
else:
checker = cast(ConditionCheckerType, factory(config))
return LegacyConditionChecker(hass, checker)
checker = factory(config)
if isinstance(checker, ConditionChecker):
return checker
return LegacyConditionChecker(hass, cast(ConditionCheckerType, checker))
async def async_and_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
) -> ConditionChecker:
"""Create multi condition matcher using 'AND'."""
checks = [await async_from_config(hass, entry) for entry in config["conditions"]]
return AndConditionChecker(hass, checks)
def if_and_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
class AndConditionChecker(CompoundConditionChecker):
"""Condition checker for 'and' compound conditions."""
def _check(self, variables: TemplateVarsType) -> bool:
"""Test and condition."""
errors = []
for index, check in enumerate(checks):
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(hass, variables) is False:
if check(self._hass, variables) is False:
return False
except ConditionError as ex:
errors.append(
ConditionErrorIndex("and", index=index, total=len(checks), error=ex)
ConditionErrorIndex(
"and", index=index, total=len(self._checks), error=ex
)
)
# Raise the errors if no check was false
@@ -1008,28 +1030,31 @@ async def async_and_from_config(
return True
return if_and_condition
async def async_or_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
) -> ConditionChecker:
"""Create multi condition matcher using 'OR'."""
checks = [await async_from_config(hass, entry) for entry in config["conditions"]]
return OrConditionChecker(hass, checks)
def if_or_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
class OrConditionChecker(CompoundConditionChecker):
"""Condition checker for 'or' compound conditions."""
def _check(self, variables: TemplateVarsType) -> bool:
"""Test or condition."""
errors = []
for index, check in enumerate(checks):
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(hass, variables) is True:
if check(self._hass, variables) is True:
return True
except ConditionError as ex:
errors.append(
ConditionErrorIndex("or", index=index, total=len(checks), error=ex)
ConditionErrorIndex(
"or", index=index, total=len(self._checks), error=ex
)
)
# Raise the errors if no check was true
@@ -1038,28 +1063,31 @@ async def async_or_from_config(
return False
return if_or_condition
async def async_not_from_config(
hass: HomeAssistant, config: ConfigType
) -> ConditionCheckerType:
) -> ConditionChecker:
"""Create multi condition matcher using 'NOT'."""
checks = [await async_from_config(hass, entry) for entry in config["conditions"]]
return NotConditionChecker(hass, checks)
def if_not_condition(
hass: HomeAssistant, variables: TemplateVarsType = None
) -> bool:
class NotConditionChecker(CompoundConditionChecker):
"""Condition checker for 'not' compound conditions."""
def _check(self, variables: TemplateVarsType) -> bool:
"""Test not condition."""
errors = []
for index, check in enumerate(checks):
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(hass, variables):
if check(self._hass, variables):
return False
except ConditionError as ex:
errors.append(
ConditionErrorIndex("not", index=index, total=len(checks), error=ex)
ConditionErrorIndex(
"not", index=index, total=len(self._checks), error=ex
)
)
# Raise the errors if no check was true
@@ -1068,8 +1096,6 @@ async def async_not_from_config(
return True
return if_not_condition
def numeric_state(
hass: HomeAssistant,
+97
View File
@@ -4346,3 +4346,100 @@ async def test_state_condition_duration_unavailable_unknown(
await hass.async_block_till_done()
freezer.tick(timedelta(seconds=11))
assert test_all(hass) is False
@pytest.mark.parametrize(
"compound_type",
["and", "or", "not"],
)
async def test_compound_condition_forwards_async_unload(
hass: HomeAssistant, compound_type: str
) -> None:
"""Test that and/or/not compound conditions forward async_unload to children."""
config = {
"condition": compound_type,
"conditions": [
{
"condition": "state",
"entity_id": "test.entity_1",
"state": STATE_ON,
},
{
"condition": "state",
"entity_id": "test.entity_2",
"state": STATE_ON,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
# The compound checker should hold child checkers
assert hasattr(test, "_checks")
assert len(test._checks) == 2
# Patch async_unload on children to verify forwarding
child_unloads = [Mock() for _ in test._checks]
for child, mock_unload in zip(test._checks, child_unloads, strict=True):
child.async_unload = mock_unload
test.async_unload()
for mock_unload in child_unloads:
mock_unload.assert_called_once()
@pytest.mark.parametrize(
("outer_type", "inner_type"),
[
(outer, inner)
for outer in ("and", "or", "not")
for inner in ("and", "or", "not")
],
)
async def test_nested_compound_condition_forwards_async_unload(
hass: HomeAssistant, outer_type: str, inner_type: str
) -> None:
"""Test that nested compound conditions forward async_unload recursively."""
config = {
"condition": outer_type,
"conditions": [
{
"condition": inner_type,
"conditions": [
{
"condition": "state",
"entity_id": "test.entity_1",
"state": STATE_ON,
},
],
},
{
"condition": "state",
"entity_id": "test.entity_2",
"state": STATE_ON,
},
],
}
config = cv.CONDITION_SCHEMA(config)
config = await condition.async_validate_condition_config(hass, config)
test = await condition.async_from_config(hass, config)
# Outer compound with 2 children: an inner compound and a leaf
assert len(test._checks) == 2
inner_checker = test._checks[0]
assert hasattr(inner_checker, "_checks")
assert len(inner_checker._checks) == 1
# Patch the innermost leaf's async_unload
innermost_unload = Mock()
inner_checker._checks[0].async_unload = innermost_unload
leaf_unload = Mock()
test._checks[1].async_unload = leaf_unload
test.async_unload()
innermost_unload.assert_called_once()
leaf_unload.assert_called_once()