mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Migrate compound conditions to ConditionChecker
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user