Refine printing of ConditionError (#46838)
* Refine printing of ConditionError * Improve coverage * name -> type
This commit is contained in:
parent
e2fd255a96
commit
d33a1a5ff8
7 changed files with 272 additions and 73 deletions
|
@ -32,7 +32,12 @@ from homeassistant.core import (
|
||||||
callback,
|
callback,
|
||||||
split_entity_id,
|
split_entity_id,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ConditionError, HomeAssistantError
|
from homeassistant.exceptions import (
|
||||||
|
ConditionError,
|
||||||
|
ConditionErrorContainer,
|
||||||
|
ConditionErrorIndex,
|
||||||
|
HomeAssistantError,
|
||||||
|
)
|
||||||
from homeassistant.helpers import condition, extract_domain_configs, template
|
from homeassistant.helpers import condition, extract_domain_configs, template
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
|
@ -616,16 +621,22 @@ async def _async_process_if(hass, config, p_config):
|
||||||
def if_action(variables=None):
|
def if_action(variables=None):
|
||||||
"""AND all conditions."""
|
"""AND all conditions."""
|
||||||
errors = []
|
errors = []
|
||||||
for check in checks:
|
for index, check in enumerate(checks):
|
||||||
try:
|
try:
|
||||||
if not check(hass, variables):
|
if not check(hass, variables):
|
||||||
return False
|
return False
|
||||||
except ConditionError as ex:
|
except ConditionError as ex:
|
||||||
errors.append(f"Error in 'condition' evaluation: {ex}")
|
errors.append(
|
||||||
|
ConditionErrorIndex(
|
||||||
|
"condition", index=index, total=len(checks), error=ex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
for error in errors:
|
LOGGER.warning(
|
||||||
LOGGER.warning("%s", error)
|
"Error evaluating condition:\n%s",
|
||||||
|
ConditionErrorContainer("condition", errors=errors),
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -112,7 +112,7 @@ async def async_attach_trigger(
|
||||||
armed_entities.add(entity_id)
|
armed_entities.add(entity_id)
|
||||||
except exceptions.ConditionError as ex:
|
except exceptions.ConditionError as ex:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Error initializing 'numeric_state' trigger for '%s': %s",
|
"Error initializing '%s' trigger: %s",
|
||||||
automation_info["name"],
|
automation_info["name"],
|
||||||
ex,
|
ex,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""The exceptions used by Home Assistant."""
|
"""The exceptions used by Home Assistant."""
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Generator, Optional, Sequence
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .core import Context # noqa: F401 pylint: disable=unused-import
|
from .core import Context # noqa: F401 pylint: disable=unused-import
|
||||||
|
@ -25,9 +27,74 @@ class TemplateError(HomeAssistantError):
|
||||||
super().__init__(f"{exception.__class__.__name__}: {exception}")
|
super().__init__(f"{exception.__class__.__name__}: {exception}")
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
class ConditionError(HomeAssistantError):
|
class ConditionError(HomeAssistantError):
|
||||||
"""Error during condition evaluation."""
|
"""Error during condition evaluation."""
|
||||||
|
|
||||||
|
# The type of the failed condition, such as 'and' or 'numeric_state'
|
||||||
|
type: str = attr.ib()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _indent(indent: int, message: str) -> str:
|
||||||
|
"""Return indentation."""
|
||||||
|
return " " * indent + message
|
||||||
|
|
||||||
|
def output(self, indent: int) -> Generator:
|
||||||
|
"""Yield an indented representation."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return string representation."""
|
||||||
|
return "\n".join(list(self.output(indent=0)))
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ConditionErrorMessage(ConditionError):
|
||||||
|
"""Condition error message."""
|
||||||
|
|
||||||
|
# A message describing this error
|
||||||
|
message: str = attr.ib()
|
||||||
|
|
||||||
|
def output(self, indent: int) -> Generator:
|
||||||
|
"""Yield an indented representation."""
|
||||||
|
yield self._indent(indent, f"In '{self.type}' condition: {self.message}")
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ConditionErrorIndex(ConditionError):
|
||||||
|
"""Condition error with index."""
|
||||||
|
|
||||||
|
# The zero-based index of the failed condition, for conditions with multiple parts
|
||||||
|
index: int = attr.ib()
|
||||||
|
# The total number of parts in this condition, including non-failed parts
|
||||||
|
total: int = attr.ib()
|
||||||
|
# The error that this error wraps
|
||||||
|
error: ConditionError = attr.ib()
|
||||||
|
|
||||||
|
def output(self, indent: int) -> Generator:
|
||||||
|
"""Yield an indented representation."""
|
||||||
|
if self.total > 1:
|
||||||
|
yield self._indent(
|
||||||
|
indent, f"In '{self.type}' (item {self.index+1} of {self.total}):"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield self._indent(indent, f"In '{self.type}':")
|
||||||
|
|
||||||
|
yield from self.error.output(indent + 1)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class ConditionErrorContainer(ConditionError):
|
||||||
|
"""Condition error with index."""
|
||||||
|
|
||||||
|
# List of ConditionErrors that this error wraps
|
||||||
|
errors: Sequence[ConditionError] = attr.ib()
|
||||||
|
|
||||||
|
def output(self, indent: int) -> Generator:
|
||||||
|
"""Yield an indented representation."""
|
||||||
|
for item in self.errors:
|
||||||
|
yield from item.output(indent)
|
||||||
|
|
||||||
|
|
||||||
class PlatformNotReady(HomeAssistantError):
|
class PlatformNotReady(HomeAssistantError):
|
||||||
"""Error to indicate that platform is not ready."""
|
"""Error to indicate that platform is not ready."""
|
||||||
|
|
|
@ -36,7 +36,14 @@ from homeassistant.const import (
|
||||||
WEEKDAYS,
|
WEEKDAYS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State, callback
|
from homeassistant.core import HomeAssistant, State, callback
|
||||||
from homeassistant.exceptions import ConditionError, HomeAssistantError, TemplateError
|
from homeassistant.exceptions import (
|
||||||
|
ConditionError,
|
||||||
|
ConditionErrorContainer,
|
||||||
|
ConditionErrorIndex,
|
||||||
|
ConditionErrorMessage,
|
||||||
|
HomeAssistantError,
|
||||||
|
TemplateError,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.sun import get_astral_event_date
|
from homeassistant.helpers.sun import get_astral_event_date
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
|
@ -109,18 +116,18 @@ async def async_and_from_config(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test and condition."""
|
"""Test and condition."""
|
||||||
errors = []
|
errors = []
|
||||||
for check in checks:
|
for index, check in enumerate(checks):
|
||||||
try:
|
try:
|
||||||
if not check(hass, variables):
|
if not check(hass, variables):
|
||||||
return False
|
return False
|
||||||
except ConditionError as ex:
|
except ConditionError as ex:
|
||||||
errors.append(str(ex))
|
errors.append(
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
ConditionErrorIndex("and", index=index, total=len(checks), error=ex)
|
||||||
errors.append(str(ex))
|
)
|
||||||
|
|
||||||
# Raise the errors if no check was false
|
# Raise the errors if no check was false
|
||||||
if errors:
|
if errors:
|
||||||
raise ConditionError("Error in 'and' condition: " + ", ".join(errors))
|
raise ConditionErrorContainer("and", errors=errors)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -142,18 +149,18 @@ async def async_or_from_config(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test or condition."""
|
"""Test or condition."""
|
||||||
errors = []
|
errors = []
|
||||||
for check in checks:
|
for index, check in enumerate(checks):
|
||||||
try:
|
try:
|
||||||
if check(hass, variables):
|
if check(hass, variables):
|
||||||
return True
|
return True
|
||||||
except ConditionError as ex:
|
except ConditionError as ex:
|
||||||
errors.append(str(ex))
|
errors.append(
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
ConditionErrorIndex("or", index=index, total=len(checks), error=ex)
|
||||||
errors.append(str(ex))
|
)
|
||||||
|
|
||||||
# Raise the errors if no check was true
|
# Raise the errors if no check was true
|
||||||
if errors:
|
if errors:
|
||||||
raise ConditionError("Error in 'or' condition: " + ", ".join(errors))
|
raise ConditionErrorContainer("or", errors=errors)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -175,18 +182,18 @@ async def async_not_from_config(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test not condition."""
|
"""Test not condition."""
|
||||||
errors = []
|
errors = []
|
||||||
for check in checks:
|
for index, check in enumerate(checks):
|
||||||
try:
|
try:
|
||||||
if check(hass, variables):
|
if check(hass, variables):
|
||||||
return False
|
return False
|
||||||
except ConditionError as ex:
|
except ConditionError as ex:
|
||||||
errors.append(str(ex))
|
errors.append(
|
||||||
except Exception as ex: # pylint: disable=broad-except
|
ConditionErrorIndex("not", index=index, total=len(checks), error=ex)
|
||||||
errors.append(str(ex))
|
)
|
||||||
|
|
||||||
# Raise the errors if no check was true
|
# Raise the errors if no check was true
|
||||||
if errors:
|
if errors:
|
||||||
raise ConditionError("Error in 'not' condition: " + ", ".join(errors))
|
raise ConditionErrorContainer("not", errors=errors)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -225,20 +232,21 @@ def async_numeric_state(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Test a numeric state condition."""
|
"""Test a numeric state condition."""
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError("No entity specified")
|
raise ConditionErrorMessage("numeric_state", "no entity specified")
|
||||||
|
|
||||||
if isinstance(entity, str):
|
if isinstance(entity, str):
|
||||||
entity_id = entity
|
entity_id = entity
|
||||||
entity = hass.states.get(entity)
|
entity = hass.states.get(entity)
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError(f"Unknown entity {entity_id}")
|
raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}")
|
||||||
else:
|
else:
|
||||||
entity_id = entity.entity_id
|
entity_id = entity.entity_id
|
||||||
|
|
||||||
if attribute is not None and attribute not in entity.attributes:
|
if attribute is not None and attribute not in entity.attributes:
|
||||||
raise ConditionError(
|
raise ConditionErrorMessage(
|
||||||
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
|
"numeric_state",
|
||||||
|
f"attribute '{attribute}' (of entity {entity_id}) does not exist",
|
||||||
)
|
)
|
||||||
|
|
||||||
value: Any = None
|
value: Any = None
|
||||||
|
@ -253,16 +261,21 @@ def async_numeric_state(
|
||||||
try:
|
try:
|
||||||
value = value_template.async_render(variables)
|
value = value_template.async_render(variables)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
raise ConditionError(f"Template error: {ex}") from ex
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state", f"template error: {ex}"
|
||||||
|
) from ex
|
||||||
|
|
||||||
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||||
raise ConditionError("State is not available")
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state", f"state of {entity_id} is unavailable"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fvalue = float(value)
|
fvalue = float(value)
|
||||||
except ValueError as ex:
|
except (ValueError, TypeError) as ex:
|
||||||
raise ConditionError(
|
raise ConditionErrorMessage(
|
||||||
f"Entity {entity_id} state '{value}' cannot be processed as a number"
|
"numeric_state",
|
||||||
|
f"entity {entity_id} state '{value}' cannot be processed as a number",
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
if below is not None:
|
if below is not None:
|
||||||
|
@ -272,9 +285,17 @@ def async_numeric_state(
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
):
|
):
|
||||||
raise ConditionError(f"The below entity {below} is not available")
|
raise ConditionErrorMessage(
|
||||||
if fvalue >= float(below_entity.state):
|
"numeric_state", f"the 'below' entity {below} is unavailable"
|
||||||
return False
|
)
|
||||||
|
try:
|
||||||
|
if fvalue >= float(below_entity.state):
|
||||||
|
return False
|
||||||
|
except (ValueError, TypeError) as ex:
|
||||||
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state",
|
||||||
|
f"the 'below' entity {below} state '{below_entity.state}' cannot be processed as a number",
|
||||||
|
) from ex
|
||||||
elif fvalue >= below:
|
elif fvalue >= below:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -285,9 +306,17 @@ def async_numeric_state(
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
):
|
):
|
||||||
raise ConditionError(f"The above entity {above} is not available")
|
raise ConditionErrorMessage(
|
||||||
if fvalue <= float(above_entity.state):
|
"numeric_state", f"the 'above' entity {above} is unavailable"
|
||||||
return False
|
)
|
||||||
|
try:
|
||||||
|
if fvalue <= float(above_entity.state):
|
||||||
|
return False
|
||||||
|
except (ValueError, TypeError) as ex:
|
||||||
|
raise ConditionErrorMessage(
|
||||||
|
"numeric_state",
|
||||||
|
f"the 'above' entity {above} state '{above_entity.state}' cannot be processed as a number",
|
||||||
|
) from ex
|
||||||
elif fvalue <= above:
|
elif fvalue <= above:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -335,20 +364,20 @@ def state(
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError("No entity specified")
|
raise ConditionErrorMessage("state", "no entity specified")
|
||||||
|
|
||||||
if isinstance(entity, str):
|
if isinstance(entity, str):
|
||||||
entity_id = entity
|
entity_id = entity
|
||||||
entity = hass.states.get(entity)
|
entity = hass.states.get(entity)
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError(f"Unknown entity {entity_id}")
|
raise ConditionErrorMessage("state", f"unknown entity {entity_id}")
|
||||||
else:
|
else:
|
||||||
entity_id = entity.entity_id
|
entity_id = entity.entity_id
|
||||||
|
|
||||||
if attribute is not None and attribute not in entity.attributes:
|
if attribute is not None and attribute not in entity.attributes:
|
||||||
raise ConditionError(
|
raise ConditionErrorMessage(
|
||||||
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
|
"state", f"attribute '{attribute}' (of entity {entity_id}) does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert isinstance(entity, State)
|
assert isinstance(entity, State)
|
||||||
|
@ -370,7 +399,9 @@ def state(
|
||||||
):
|
):
|
||||||
state_entity = hass.states.get(req_state_value)
|
state_entity = hass.states.get(req_state_value)
|
||||||
if not state_entity:
|
if not state_entity:
|
||||||
continue
|
raise ConditionErrorMessage(
|
||||||
|
"state", f"the 'state' entity {req_state_value} is unavailable"
|
||||||
|
)
|
||||||
state_value = state_entity.state
|
state_value = state_entity.state
|
||||||
is_state = value == state_value
|
is_state = value == state_value
|
||||||
if is_state:
|
if is_state:
|
||||||
|
@ -495,7 +526,7 @@ def async_template(
|
||||||
try:
|
try:
|
||||||
value: str = value_template.async_render(variables, parse_result=False)
|
value: str = value_template.async_render(variables, parse_result=False)
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
raise ConditionError(f"Error in 'template' condition: {ex}") from ex
|
raise ConditionErrorMessage("template", str(ex)) from ex
|
||||||
|
|
||||||
return value.lower() == "true"
|
return value.lower() == "true"
|
||||||
|
|
||||||
|
@ -538,9 +569,7 @@ def time(
|
||||||
elif isinstance(after, str):
|
elif isinstance(after, str):
|
||||||
after_entity = hass.states.get(after)
|
after_entity = hass.states.get(after)
|
||||||
if not after_entity:
|
if not after_entity:
|
||||||
raise ConditionError(
|
raise ConditionErrorMessage("time", f"unknown 'after' entity {after}")
|
||||||
f"Error in 'time' condition: The 'after' entity {after} is not available"
|
|
||||||
)
|
|
||||||
after = dt_util.dt.time(
|
after = dt_util.dt.time(
|
||||||
after_entity.attributes.get("hour", 23),
|
after_entity.attributes.get("hour", 23),
|
||||||
after_entity.attributes.get("minute", 59),
|
after_entity.attributes.get("minute", 59),
|
||||||
|
@ -552,9 +581,7 @@ def time(
|
||||||
elif isinstance(before, str):
|
elif isinstance(before, str):
|
||||||
before_entity = hass.states.get(before)
|
before_entity = hass.states.get(before)
|
||||||
if not before_entity:
|
if not before_entity:
|
||||||
raise ConditionError(
|
raise ConditionErrorMessage("time", f"unknown 'before' entity {before}")
|
||||||
f"Error in 'time' condition: The 'before' entity {before} is not available"
|
|
||||||
)
|
|
||||||
before = dt_util.dt.time(
|
before = dt_util.dt.time(
|
||||||
before_entity.attributes.get("hour", 23),
|
before_entity.attributes.get("hour", 23),
|
||||||
before_entity.attributes.get("minute", 59),
|
before_entity.attributes.get("minute", 59),
|
||||||
|
@ -609,24 +636,24 @@ def zone(
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
if zone_ent is None:
|
if zone_ent is None:
|
||||||
raise ConditionError("No zone specified")
|
raise ConditionErrorMessage("zone", "no zone specified")
|
||||||
|
|
||||||
if isinstance(zone_ent, str):
|
if isinstance(zone_ent, str):
|
||||||
zone_ent_id = zone_ent
|
zone_ent_id = zone_ent
|
||||||
zone_ent = hass.states.get(zone_ent)
|
zone_ent = hass.states.get(zone_ent)
|
||||||
|
|
||||||
if zone_ent is None:
|
if zone_ent is None:
|
||||||
raise ConditionError(f"Unknown zone {zone_ent_id}")
|
raise ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}")
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError("No entity specified")
|
raise ConditionErrorMessage("zone", "no entity specified")
|
||||||
|
|
||||||
if isinstance(entity, str):
|
if isinstance(entity, str):
|
||||||
entity_id = entity
|
entity_id = entity
|
||||||
entity = hass.states.get(entity)
|
entity = hass.states.get(entity)
|
||||||
|
|
||||||
if entity is None:
|
if entity is None:
|
||||||
raise ConditionError(f"Unknown entity {entity_id}")
|
raise ConditionErrorMessage("zone", f"unknown entity {entity_id}")
|
||||||
else:
|
else:
|
||||||
entity_id = entity.entity_id
|
entity_id = entity.entity_id
|
||||||
|
|
||||||
|
@ -634,10 +661,14 @@ def zone(
|
||||||
longitude = entity.attributes.get(ATTR_LONGITUDE)
|
longitude = entity.attributes.get(ATTR_LONGITUDE)
|
||||||
|
|
||||||
if latitude is None:
|
if latitude is None:
|
||||||
raise ConditionError(f"Entity {entity_id} has no 'latitude' attribute")
|
raise ConditionErrorMessage(
|
||||||
|
"zone", f"entity {entity_id} has no 'latitude' attribute"
|
||||||
|
)
|
||||||
|
|
||||||
if longitude is None:
|
if longitude is None:
|
||||||
raise ConditionError(f"Entity {entity_id} has no 'longitude' attribute")
|
raise ConditionErrorMessage(
|
||||||
|
"zone", f"entity {entity_id} has no 'longitude' attribute"
|
||||||
|
)
|
||||||
|
|
||||||
return zone_cmp.in_zone(
|
return zone_cmp.in_zone(
|
||||||
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
|
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
|
||||||
|
@ -664,15 +695,20 @@ def zone_from_config(
|
||||||
try:
|
try:
|
||||||
if zone(hass, zone_entity_id, entity_id):
|
if zone(hass, zone_entity_id, entity_id):
|
||||||
entity_ok = True
|
entity_ok = True
|
||||||
except ConditionError as ex:
|
except ConditionErrorMessage as ex:
|
||||||
errors.append(str(ex))
|
errors.append(
|
||||||
|
ConditionErrorMessage(
|
||||||
|
"zone",
|
||||||
|
f"error matching {entity_id} with {zone_entity_id}: {ex.message}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not entity_ok:
|
if not entity_ok:
|
||||||
all_ok = False
|
all_ok = False
|
||||||
|
|
||||||
# Raise the errors only if no definitive result was found
|
# Raise the errors only if no definitive result was found
|
||||||
if errors and not all_ok:
|
if errors and not all_ok:
|
||||||
raise ConditionError("Error in 'zone' condition: " + ", ".join(errors))
|
raise ConditionErrorContainer("zone", errors=errors)
|
||||||
|
|
||||||
return all_ok
|
return all_ok
|
||||||
|
|
||||||
|
|
|
@ -516,7 +516,7 @@ class _ScriptRun:
|
||||||
try:
|
try:
|
||||||
check = cond(self._hass, self._variables)
|
check = cond(self._hass, self._variables)
|
||||||
except exceptions.ConditionError as ex:
|
except exceptions.ConditionError as ex:
|
||||||
_LOGGER.warning("Error in 'condition' evaluation: %s", ex)
|
_LOGGER.warning("Error in 'condition' evaluation:\n%s", ex)
|
||||||
check = False
|
check = False
|
||||||
|
|
||||||
self._log("Test condition %s: %s", self._script.last_action, check)
|
self._log("Test condition %s: %s", self._script.last_action, check)
|
||||||
|
@ -575,7 +575,7 @@ class _ScriptRun:
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
except exceptions.ConditionError as ex:
|
except exceptions.ConditionError as ex:
|
||||||
_LOGGER.warning("Error in 'while' evaluation: %s", ex)
|
_LOGGER.warning("Error in 'while' evaluation:\n%s", ex)
|
||||||
break
|
break
|
||||||
|
|
||||||
await async_run_sequence(iteration)
|
await async_run_sequence(iteration)
|
||||||
|
@ -593,7 +593,7 @@ class _ScriptRun:
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
except exceptions.ConditionError as ex:
|
except exceptions.ConditionError as ex:
|
||||||
_LOGGER.warning("Error in 'until' evaluation: %s", ex)
|
_LOGGER.warning("Error in 'until' evaluation:\n%s", ex)
|
||||||
break
|
break
|
||||||
|
|
||||||
if saved_repeat_vars:
|
if saved_repeat_vars:
|
||||||
|
@ -614,7 +614,7 @@ class _ScriptRun:
|
||||||
await self._async_run_script(script)
|
await self._async_run_script(script)
|
||||||
return
|
return
|
||||||
except exceptions.ConditionError as ex:
|
except exceptions.ConditionError as ex:
|
||||||
_LOGGER.warning("Error in 'choose' evaluation: %s", ex)
|
_LOGGER.warning("Error in 'choose' evaluation:\n%s", ex)
|
||||||
|
|
||||||
if choose_data["default"]:
|
if choose_data["default"]:
|
||||||
await self._async_run_script(choose_data["default"])
|
await self._async_run_script(choose_data["default"])
|
||||||
|
|
|
@ -386,8 +386,12 @@ async def test_if_numeric_state_raises_on_unavailable(hass, caplog):
|
||||||
|
|
||||||
async def test_state_raises(hass):
|
async def test_state_raises(hass):
|
||||||
"""Test that state raises ConditionError on errors."""
|
"""Test that state raises ConditionError on errors."""
|
||||||
|
# No entity
|
||||||
|
with pytest.raises(ConditionError, match="no entity"):
|
||||||
|
condition.state(hass, entity=None, req_state="missing")
|
||||||
|
|
||||||
# Unknown entity_id
|
# Unknown entity_id
|
||||||
with pytest.raises(ConditionError, match="Unknown entity"):
|
with pytest.raises(ConditionError, match="unknown entity"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -400,7 +404,7 @@ async def test_state_raises(hass):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
# Unknown attribute
|
# Unknown attribute
|
||||||
with pytest.raises(ConditionError, match=r"Attribute .* does not exist"):
|
with pytest.raises(ConditionError, match=r"attribute .* does not exist"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -414,6 +418,20 @@ async def test_state_raises(hass):
|
||||||
hass.states.async_set("sensor.door", "open")
|
hass.states.async_set("sensor.door", "open")
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
|
# Unknown state entity
|
||||||
|
with pytest.raises(ConditionError, match="input_text.missing"):
|
||||||
|
test = await condition.async_from_config(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"condition": "state",
|
||||||
|
"entity_id": "sensor.door",
|
||||||
|
"state": "input_text.missing",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.states.async_set("sensor.door", "open")
|
||||||
|
test(hass)
|
||||||
|
|
||||||
|
|
||||||
async def test_state_multiple_entities(hass):
|
async def test_state_multiple_entities(hass):
|
||||||
"""Test with multiple entities in condition."""
|
"""Test with multiple entities in condition."""
|
||||||
|
@ -564,7 +582,6 @@ async def test_state_using_input_entities(hass):
|
||||||
"state": [
|
"state": [
|
||||||
"input_text.hello",
|
"input_text.hello",
|
||||||
"input_select.hello",
|
"input_select.hello",
|
||||||
"input_number.not_exist",
|
|
||||||
"salut",
|
"salut",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -616,7 +633,7 @@ async def test_state_using_input_entities(hass):
|
||||||
async def test_numeric_state_raises(hass):
|
async def test_numeric_state_raises(hass):
|
||||||
"""Test that numeric_state raises ConditionError on errors."""
|
"""Test that numeric_state raises ConditionError on errors."""
|
||||||
# Unknown entity_id
|
# Unknown entity_id
|
||||||
with pytest.raises(ConditionError, match="Unknown entity"):
|
with pytest.raises(ConditionError, match="unknown entity"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -629,7 +646,7 @@ async def test_numeric_state_raises(hass):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
# Unknown attribute
|
# Unknown attribute
|
||||||
with pytest.raises(ConditionError, match=r"Attribute .* does not exist"):
|
with pytest.raises(ConditionError, match=r"attribute .* does not exist"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -659,7 +676,7 @@ async def test_numeric_state_raises(hass):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
# Unavailable state
|
# Unavailable state
|
||||||
with pytest.raises(ConditionError, match="State is not available"):
|
with pytest.raises(ConditionError, match="state of .* is unavailable"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -687,7 +704,7 @@ async def test_numeric_state_raises(hass):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
# Below entity missing
|
# Below entity missing
|
||||||
with pytest.raises(ConditionError, match="below entity"):
|
with pytest.raises(ConditionError, match="'below' entity"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -700,8 +717,16 @@ async def test_numeric_state_raises(hass):
|
||||||
hass.states.async_set("sensor.temperature", 50)
|
hass.states.async_set("sensor.temperature", 50)
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
|
# Below entity not a number
|
||||||
|
with pytest.raises(
|
||||||
|
ConditionError,
|
||||||
|
match="'below'.*input_number.missing.*cannot be processed as a number",
|
||||||
|
):
|
||||||
|
hass.states.async_set("input_number.missing", "number")
|
||||||
|
test(hass)
|
||||||
|
|
||||||
# Above entity missing
|
# Above entity missing
|
||||||
with pytest.raises(ConditionError, match="above entity"):
|
with pytest.raises(ConditionError, match="'above' entity"):
|
||||||
test = await condition.async_from_config(
|
test = await condition.async_from_config(
|
||||||
hass,
|
hass,
|
||||||
{
|
{
|
||||||
|
@ -714,6 +739,14 @@ async def test_numeric_state_raises(hass):
|
||||||
hass.states.async_set("sensor.temperature", 50)
|
hass.states.async_set("sensor.temperature", 50)
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
|
# Above entity not a number
|
||||||
|
with pytest.raises(
|
||||||
|
ConditionError,
|
||||||
|
match="'above'.*input_number.missing.*cannot be processed as a number",
|
||||||
|
):
|
||||||
|
hass.states.async_set("input_number.missing", "number")
|
||||||
|
test(hass)
|
||||||
|
|
||||||
|
|
||||||
async def test_numeric_state_multiple_entities(hass):
|
async def test_numeric_state_multiple_entities(hass):
|
||||||
"""Test with multiple entities in condition."""
|
"""Test with multiple entities in condition."""
|
||||||
|
@ -849,7 +882,10 @@ async def test_zone_raises(hass):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ConditionError, match="Unknown zone"):
|
with pytest.raises(ConditionError, match="no zone"):
|
||||||
|
condition.zone(hass, zone_ent=None, entity="sensor.any")
|
||||||
|
|
||||||
|
with pytest.raises(ConditionError, match="unknown zone"):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
@ -858,7 +894,10 @@ async def test_zone_raises(hass):
|
||||||
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
|
{"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10},
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ConditionError, match="Unknown entity"):
|
with pytest.raises(ConditionError, match="no entity"):
|
||||||
|
condition.zone(hass, zone_ent="zone.home", entity=None)
|
||||||
|
|
||||||
|
with pytest.raises(ConditionError, match="unknown entity"):
|
||||||
test(hass)
|
test(hass)
|
||||||
|
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
|
|
46
tests/test_exceptions.py
Normal file
46
tests/test_exceptions.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
"""Test to verify that Home Assistant exceptions work."""
|
||||||
|
from homeassistant.exceptions import (
|
||||||
|
ConditionErrorContainer,
|
||||||
|
ConditionErrorIndex,
|
||||||
|
ConditionErrorMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_conditionerror_format():
|
||||||
|
"""Test ConditionError stringifiers."""
|
||||||
|
error1 = ConditionErrorMessage("test", "A test error")
|
||||||
|
assert str(error1) == "In 'test' condition: A test error"
|
||||||
|
|
||||||
|
error2 = ConditionErrorMessage("test", "Another error")
|
||||||
|
assert str(error2) == "In 'test' condition: Another error"
|
||||||
|
|
||||||
|
error_pos1 = ConditionErrorIndex("box", index=0, total=2, error=error1)
|
||||||
|
assert (
|
||||||
|
str(error_pos1)
|
||||||
|
== """In 'box' (item 1 of 2):
|
||||||
|
In 'test' condition: A test error"""
|
||||||
|
)
|
||||||
|
|
||||||
|
error_pos2 = ConditionErrorIndex("box", index=1, total=2, error=error2)
|
||||||
|
assert (
|
||||||
|
str(error_pos2)
|
||||||
|
== """In 'box' (item 2 of 2):
|
||||||
|
In 'test' condition: Another error"""
|
||||||
|
)
|
||||||
|
|
||||||
|
error_container1 = ConditionErrorContainer("box", errors=[error_pos1, error_pos2])
|
||||||
|
print(error_container1)
|
||||||
|
assert (
|
||||||
|
str(error_container1)
|
||||||
|
== """In 'box' (item 1 of 2):
|
||||||
|
In 'test' condition: A test error
|
||||||
|
In 'box' (item 2 of 2):
|
||||||
|
In 'test' condition: Another error"""
|
||||||
|
)
|
||||||
|
|
||||||
|
error_pos3 = ConditionErrorIndex("box", index=0, total=1, error=error1)
|
||||||
|
assert (
|
||||||
|
str(error_pos3)
|
||||||
|
== """In 'box':
|
||||||
|
In 'test' condition: A test error"""
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue