Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 61 additions & 12 deletions homeassistant/helpers/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(self._hass, **kwargs) is False:
if check.async_check(**kwargs) is False:
return False
except ConditionError as ex:
errors.append(
Expand Down Expand Up @@ -1049,7 +1049,7 @@ def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(self._hass, **kwargs) is True:
if check.async_check(**kwargs) is True:
return True
except ConditionError as ex:
errors.append(
Expand Down Expand Up @@ -1083,7 +1083,7 @@ def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
for index, check in enumerate(self._checks):
try:
with trace_path(["conditions", str(index)]):
if check(self._hass, **kwargs):
if check.async_check(**kwargs):
return False
except ConditionError as ex:
errors.append(
Expand Down Expand Up @@ -1646,40 +1646,89 @@ async def async_conditions_from_config(
condition_configs: list[ConfigType],
logger: logging.Logger,
name: str,
) -> Callable[[TemplateVarsType], bool]:
) -> ConditionsChecker:
"""AND all conditions."""
checks = [
await async_from_config(hass, condition_config)
for condition_config in condition_configs
]
return ConditionsChecker(hass, checks, logger, name)

def check_conditions(variables: TemplateVarsType = None) -> bool:

class ConditionsChecker(ConditionChecker):
"""Condition checker that ANDs multiple conditions.

Used by automations and template entities. Unlike AndConditionChecker,
this logs warnings on errors instead of raising, and uses "condition"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe specify where the errors happen and are now caught? Looks like the change was done on unload.

as the trace path prefix.
"""

def __init__(
self,
hass: HomeAssistant,
checks: list[ConditionChecker],
logger: logging.Logger,
name: str,
) -> None:
"""Initialize condition checker."""
super().__init__(hass)
self._checks = checks
Comment thread
emontnemery marked this conversation as resolved.
Outdated
self._logger = logger
self._name = name

# The function previously returned by async_conditions_from_config
# did not accept hass as first argument, so we need to override the
# __call__ provided for backwards compatibility
def __call__( # type: ignore[override]
self,
variables: TemplateVarsType | None = None,
**kwargs: Any,
Comment thread
arturpragacz marked this conversation as resolved.
Outdated
) -> bool:
"""Check all conditions."""
return self.async_check(variables=variables)

def async_unload(self) -> None:
"""Clean up child conditions."""
for check in self._checks:
check.async_unload()
super().async_unload()

def async_check(
self, *, variables: TemplateVarsType = None, **kwargs: Never
) -> bool:
"""Check the condition.

Overrides the base class to skip the outer trace_condition wrapper.
The individual child conditions handle their own tracing.
"""
return self._async_check(variables=variables)

@callback
def _async_check(self, **kwargs: Unpack[ConditionCheckParams]) -> bool:
"""AND all conditions."""
errors: list[ConditionErrorIndex] = []
for index, check in enumerate(checks):
for index, check in enumerate(self._checks):
try:
with trace_path(["condition", str(index)]):
if check(hass, variables) is False:
if check.async_check(**kwargs) is False:
return False
except ConditionError as ex:
errors.append(
ConditionErrorIndex(
"condition", index=index, total=len(checks), error=ex
"condition", index=index, total=len(self._checks), error=ex
)
)

if errors:
logger.warning(
self._logger.warning(
"Error evaluating condition in '%s':\n%s",
name,
self._name,
ConditionErrorContainer("condition", errors=errors),
)
return False

return True

return check_conditions


@callback
def async_extract_entities(config: ConfigType | Template) -> set[str]:
Expand Down
57 changes: 57 additions & 0 deletions tests/helpers/test_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from contextlib import AbstractContextManager, nullcontext as does_not_raise
from datetime import timedelta
import io
import logging
from typing import Any
from unittest.mock import AsyncMock, Mock, patch

Expand Down Expand Up @@ -4488,3 +4489,59 @@ async def test_nested_compound_condition_forwards_async_unload(

test._checks[0]._checks[0].async_unload.assert_called_once()
test._checks[1].async_unload.assert_called_once()


async def test_conditions_from_config_forwards_async_unload(
hass: HomeAssistant,
) -> None:
"""Test that async_conditions_from_config forwards async_unload to children."""
await _setup_mock_integration(hass)
configs = [
await condition.async_validate_condition_config(hass, {"condition": "test"}),
await condition.async_validate_condition_config(hass, {"condition": "test"}),
]
test = await condition.async_conditions_from_config(
hass, configs, logging.getLogger(__name__), "test"
)

assert hasattr(test, "_checks")
assert len(test._checks) == 2

test.async_unload()

for child in test._checks:
child.async_unload.assert_called_once()


Comment thread
emontnemery marked this conversation as resolved.
@pytest.mark.parametrize(
"inner_type",
["and", "or", "not"],
)
async def test_conditions_from_config_nested_forwards_async_unload(
hass: HomeAssistant, inner_type: str
) -> None:
"""Test that async_conditions_from_config forwards async_unload recursively."""
await _setup_mock_integration(hass)
configs = [
await condition.async_validate_condition_config(
hass,
{
"condition": inner_type,
"conditions": [{"condition": "test"}],
},
),
await condition.async_validate_condition_config(hass, {"condition": "test"}),
]
test = await condition.async_conditions_from_config(
hass, configs, logging.getLogger(__name__), "test"
)

assert len(test._checks) == 2
inner_checker = test._checks[0]
assert hasattr(inner_checker, "_checks")
assert len(inner_checker._checks) == 1

test.async_unload()

test._checks[0]._checks[0].async_unload.assert_called_once()
test._checks[1].async_unload.assert_called_once()