mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Refine printing of ConditionError (#46838)
* Refine printing of ConditionError * Improve coverage * name -> type
This commit is contained in:
parent
e2fd255a96
commit
d33a1a5ff8
@ -32,7 +32,12 @@ from homeassistant.core import (
|
||||
callback,
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
@ -616,16 +621,22 @@ async def _async_process_if(hass, config, p_config):
|
||||
def if_action(variables=None):
|
||||
"""AND all conditions."""
|
||||
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(f"Error in 'condition' evaluation: {ex}")
|
||||
errors.append(
|
||||
ConditionErrorIndex(
|
||||
"condition", index=index, total=len(checks), error=ex
|
||||
)
|
||||
)
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
LOGGER.warning("%s", error)
|
||||
LOGGER.warning(
|
||||
"Error evaluating condition:\n%s",
|
||||
ConditionErrorContainer("condition", errors=errors),
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -112,7 +112,7 @@ async def async_attach_trigger(
|
||||
armed_entities.add(entity_id)
|
||||
except exceptions.ConditionError as ex:
|
||||
_LOGGER.warning(
|
||||
"Error initializing 'numeric_state' trigger for '%s': %s",
|
||||
"Error initializing '%s' trigger: %s",
|
||||
automation_info["name"],
|
||||
ex,
|
||||
)
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""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:
|
||||
from .core import Context # noqa: F401 pylint: disable=unused-import
|
||||
@ -25,9 +27,74 @@ class TemplateError(HomeAssistantError):
|
||||
super().__init__(f"{exception.__class__.__name__}: {exception}")
|
||||
|
||||
|
||||
@attr.s
|
||||
class ConditionError(HomeAssistantError):
|
||||
"""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):
|
||||
"""Error to indicate that platform is not ready."""
|
||||
|
@ -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
|
||||
|
||||
|
@ -516,7 +516,7 @@ class _ScriptRun:
|
||||
try:
|
||||
check = cond(self._hass, self._variables)
|
||||
except exceptions.ConditionError as ex:
|
||||
_LOGGER.warning("Error in 'condition' evaluation: %s", ex)
|
||||
_LOGGER.warning("Error in 'condition' evaluation:\n%s", ex)
|
||||
check = False
|
||||
|
||||
self._log("Test condition %s: %s", self._script.last_action, check)
|
||||
@ -575,7 +575,7 @@ class _ScriptRun:
|
||||
):
|
||||
break
|
||||
except exceptions.ConditionError as ex:
|
||||
_LOGGER.warning("Error in 'while' evaluation: %s", ex)
|
||||
_LOGGER.warning("Error in 'while' evaluation:\n%s", ex)
|
||||
break
|
||||
|
||||
await async_run_sequence(iteration)
|
||||
@ -593,7 +593,7 @@ class _ScriptRun:
|
||||
):
|
||||
break
|
||||
except exceptions.ConditionError as ex:
|
||||
_LOGGER.warning("Error in 'until' evaluation: %s", ex)
|
||||
_LOGGER.warning("Error in 'until' evaluation:\n%s", ex)
|
||||
break
|
||||
|
||||
if saved_repeat_vars:
|
||||
@ -614,7 +614,7 @@ class _ScriptRun:
|
||||
await self._async_run_script(script)
|
||||
return
|
||||
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"]:
|
||||
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):
|
||||
"""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
|
||||
with pytest.raises(ConditionError, match="Unknown entity"):
|
||||
with pytest.raises(ConditionError, match="unknown entity"):
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
@ -400,7 +404,7 @@ async def test_state_raises(hass):
|
||||
test(hass)
|
||||
|
||||
# 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(
|
||||
hass,
|
||||
{
|
||||
@ -414,6 +418,20 @@ async def test_state_raises(hass):
|
||||
hass.states.async_set("sensor.door", "open")
|
||||
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):
|
||||
"""Test with multiple entities in condition."""
|
||||
@ -564,7 +582,6 @@ async def test_state_using_input_entities(hass):
|
||||
"state": [
|
||||
"input_text.hello",
|
||||
"input_select.hello",
|
||||
"input_number.not_exist",
|
||||
"salut",
|
||||
],
|
||||
},
|
||||
@ -616,7 +633,7 @@ async def test_state_using_input_entities(hass):
|
||||
async def test_numeric_state_raises(hass):
|
||||
"""Test that numeric_state raises ConditionError on errors."""
|
||||
# Unknown entity_id
|
||||
with pytest.raises(ConditionError, match="Unknown entity"):
|
||||
with pytest.raises(ConditionError, match="unknown entity"):
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
@ -629,7 +646,7 @@ async def test_numeric_state_raises(hass):
|
||||
test(hass)
|
||||
|
||||
# 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(
|
||||
hass,
|
||||
{
|
||||
@ -659,7 +676,7 @@ async def test_numeric_state_raises(hass):
|
||||
test(hass)
|
||||
|
||||
# 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(
|
||||
hass,
|
||||
{
|
||||
@ -687,7 +704,7 @@ async def test_numeric_state_raises(hass):
|
||||
test(hass)
|
||||
|
||||
# Below entity missing
|
||||
with pytest.raises(ConditionError, match="below entity"):
|
||||
with pytest.raises(ConditionError, match="'below' entity"):
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
@ -700,8 +717,16 @@ async def test_numeric_state_raises(hass):
|
||||
hass.states.async_set("sensor.temperature", 50)
|
||||
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
|
||||
with pytest.raises(ConditionError, match="above entity"):
|
||||
with pytest.raises(ConditionError, match="'above' entity"):
|
||||
test = await condition.async_from_config(
|
||||
hass,
|
||||
{
|
||||
@ -714,6 +739,14 @@ async def test_numeric_state_raises(hass):
|
||||
hass.states.async_set("sensor.temperature", 50)
|
||||
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):
|
||||
"""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)
|
||||
|
||||
hass.states.async_set(
|
||||
@ -858,7 +894,10 @@ async def test_zone_raises(hass):
|
||||
{"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)
|
||||
|
||||
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…
x
Reference in New Issue
Block a user