Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
51 changes: 42 additions & 9 deletions homeassistant/helpers/condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1646,40 +1646,73 @@ 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(CompoundConditionChecker):
Comment thread
arturpragacz marked this conversation as resolved.
Outdated
"""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, checks)
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 backwawrds comaptibitility
Comment thread
emontnemery marked this conversation as resolved.
Outdated
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."""
# The implicit AND can't be disabled, so the call will never return None
return self.async_check(variables=variables) # type: ignore[return-value]
Comment thread
arturpragacz marked this conversation as resolved.
Outdated

@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(self._hass, **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()
Loading