Refine printing of ConditionError (#46838)

* Refine printing of ConditionError

* Improve coverage

* name -> type
This commit is contained in:
Anders Melchiorsen 2021-02-21 14:54:36 +01:00 committed by GitHub
parent e2fd255a96
commit d33a1a5ff8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 272 additions and 73 deletions

View file

@ -36,7 +36,14 @@ from homeassistant.const import (
WEEKDAYS,
)
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
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.helpers.template import Template
@ -109,18 +116,18 @@ async def async_and_from_config(
) -> bool:
"""Test and condition."""
errors = []
for check in checks:
for index, check in enumerate(checks):
try:
if not check(hass, variables):
return False
except ConditionError as ex:
errors.append(str(ex))
except Exception as ex: # pylint: disable=broad-except
errors.append(str(ex))
errors.append(
ConditionErrorIndex("and", index=index, total=len(checks), error=ex)
)
# Raise the errors if no check was false
if errors:
raise ConditionError("Error in 'and' condition: " + ", ".join(errors))
raise ConditionErrorContainer("and", errors=errors)
return True
@ -142,18 +149,18 @@ async def async_or_from_config(
) -> bool:
"""Test or condition."""
errors = []
for check in checks:
for index, check in enumerate(checks):
try:
if check(hass, variables):
return True
except ConditionError as ex:
errors.append(str(ex))
except Exception as ex: # pylint: disable=broad-except
errors.append(str(ex))
errors.append(
ConditionErrorIndex("or", index=index, total=len(checks), error=ex)
)
# Raise the errors if no check was true
if errors:
raise ConditionError("Error in 'or' condition: " + ", ".join(errors))
raise ConditionErrorContainer("or", errors=errors)
return False
@ -175,18 +182,18 @@ async def async_not_from_config(
) -> bool:
"""Test not condition."""
errors = []
for check in checks:
for index, check in enumerate(checks):
try:
if check(hass, variables):
return False
except ConditionError as ex:
errors.append(str(ex))
except Exception as ex: # pylint: disable=broad-except
errors.append(str(ex))
errors.append(
ConditionErrorIndex("not", index=index, total=len(checks), error=ex)
)
# Raise the errors if no check was true
if errors:
raise ConditionError("Error in 'not' condition: " + ", ".join(errors))
raise ConditionErrorContainer("not", errors=errors)
return True
@ -225,20 +232,21 @@ def async_numeric_state(
) -> bool:
"""Test a numeric state condition."""
if entity is None:
raise ConditionError("No entity specified")
raise ConditionErrorMessage("numeric_state", "no entity specified")
if isinstance(entity, str):
entity_id = entity
entity = hass.states.get(entity)
if entity is None:
raise ConditionError(f"Unknown entity {entity_id}")
raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}")
else:
entity_id = entity.entity_id
if attribute is not None and attribute not in entity.attributes:
raise ConditionError(
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
raise ConditionErrorMessage(
"numeric_state",
f"attribute '{attribute}' (of entity {entity_id}) does not exist",
)
value: Any = None
@ -253,16 +261,21 @@ def async_numeric_state(
try:
value = value_template.async_render(variables)
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):
raise ConditionError("State is not available")
raise ConditionErrorMessage(
"numeric_state", f"state of {entity_id} is unavailable"
)
try:
fvalue = float(value)
except ValueError as ex:
raise ConditionError(
f"Entity {entity_id} state '{value}' cannot be processed as a number"
except (ValueError, TypeError) as ex:
raise ConditionErrorMessage(
"numeric_state",
f"entity {entity_id} state '{value}' cannot be processed as a number",
) from ex
if below is not None:
@ -272,9 +285,17 @@ def async_numeric_state(
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
raise ConditionError(f"The below entity {below} is not available")
if fvalue >= float(below_entity.state):
return False
raise ConditionErrorMessage(
"numeric_state", f"the 'below' entity {below} is unavailable"
)
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:
return False
@ -285,9 +306,17 @@ def async_numeric_state(
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
raise ConditionError(f"The above entity {above} is not available")
if fvalue <= float(above_entity.state):
return False
raise ConditionErrorMessage(
"numeric_state", f"the 'above' entity {above} is unavailable"
)
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:
return False
@ -335,20 +364,20 @@ def state(
Async friendly.
"""
if entity is None:
raise ConditionError("No entity specified")
raise ConditionErrorMessage("state", "no entity specified")
if isinstance(entity, str):
entity_id = entity
entity = hass.states.get(entity)
if entity is None:
raise ConditionError(f"Unknown entity {entity_id}")
raise ConditionErrorMessage("state", f"unknown entity {entity_id}")
else:
entity_id = entity.entity_id
if attribute is not None and attribute not in entity.attributes:
raise ConditionError(
f"Attribute '{attribute}' (of entity {entity_id}) does not exist"
raise ConditionErrorMessage(
"state", f"attribute '{attribute}' (of entity {entity_id}) does not exist"
)
assert isinstance(entity, State)
@ -370,7 +399,9 @@ def state(
):
state_entity = hass.states.get(req_state_value)
if not state_entity:
continue
raise ConditionErrorMessage(
"state", f"the 'state' entity {req_state_value} is unavailable"
)
state_value = state_entity.state
is_state = value == state_value
if is_state:
@ -495,7 +526,7 @@ def async_template(
try:
value: str = value_template.async_render(variables, parse_result=False)
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"
@ -538,9 +569,7 @@ def time(
elif isinstance(after, str):
after_entity = hass.states.get(after)
if not after_entity:
raise ConditionError(
f"Error in 'time' condition: The 'after' entity {after} is not available"
)
raise ConditionErrorMessage("time", f"unknown 'after' entity {after}")
after = dt_util.dt.time(
after_entity.attributes.get("hour", 23),
after_entity.attributes.get("minute", 59),
@ -552,9 +581,7 @@ def time(
elif isinstance(before, str):
before_entity = hass.states.get(before)
if not before_entity:
raise ConditionError(
f"Error in 'time' condition: The 'before' entity {before} is not available"
)
raise ConditionErrorMessage("time", f"unknown 'before' entity {before}")
before = dt_util.dt.time(
before_entity.attributes.get("hour", 23),
before_entity.attributes.get("minute", 59),
@ -609,24 +636,24 @@ def zone(
Async friendly.
"""
if zone_ent is None:
raise ConditionError("No zone specified")
raise ConditionErrorMessage("zone", "no zone specified")
if isinstance(zone_ent, str):
zone_ent_id = zone_ent
zone_ent = hass.states.get(zone_ent)
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:
raise ConditionError("No entity specified")
raise ConditionErrorMessage("zone", "no entity specified")
if isinstance(entity, str):
entity_id = entity
entity = hass.states.get(entity)
if entity is None:
raise ConditionError(f"Unknown entity {entity_id}")
raise ConditionErrorMessage("zone", f"unknown entity {entity_id}")
else:
entity_id = entity.entity_id
@ -634,10 +661,14 @@ def zone(
longitude = entity.attributes.get(ATTR_LONGITUDE)
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:
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(
zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0)
@ -664,15 +695,20 @@ def zone_from_config(
try:
if zone(hass, zone_entity_id, entity_id):
entity_ok = True
except ConditionError as ex:
errors.append(str(ex))
except ConditionErrorMessage as ex:
errors.append(
ConditionErrorMessage(
"zone",
f"error matching {entity_id} with {zone_entity_id}: {ex.message}",
)
)
if not entity_ok:
all_ok = False
# Raise the errors only if no definitive result was found
if errors and not all_ok:
raise ConditionError("Error in 'zone' condition: " + ", ".join(errors))
raise ConditionErrorContainer("zone", errors=errors)
return all_ok