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]) 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""")
def time( hass: HomeAssistant, before: dt_util.dt.time | str | None = None, after: dt_util.dt.time | str | None = None, weekday: None | str | Container[str] = None, ) -> bool: """Test if local time condition matches. Handle the fact that time is continuous and we may be testing for a period that crosses midnight. In that case it is easier to test for the opposite. "(23:59 <= now < 00:01)" would be the same as "not (00:01 <= now < 23:59)". """ now = dt_util.now() now_time = now.time() if after is None: after = dt_util.dt.time(0) elif isinstance(after, str): after_entity = hass.states.get(after) if not after_entity: 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), after_entity.attributes.get("second", 59), ) if before is None: before = dt_util.dt.time(23, 59, 59, 999999) elif isinstance(before, str): before_entity = hass.states.get(before) if not before_entity: 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), before_entity.attributes.get("second", 59), 999999, ) if after < before: if not after <= now_time < before: return False else: if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] if ( isinstance(weekday, str) and weekday != now_weekday or now_weekday not in weekday ): return False return True
def state( hass: HomeAssistant, entity: Union[None, str, State], req_state: Any, for_period: Optional[timedelta] = None, attribute: Optional[str] = None, ) -> bool: """Test if state matches requirements. Async friendly. """ if entity is None: raise ConditionErrorMessage("state", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: 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 ConditionErrorMessage( "state", f"attribute '{attribute}' (of entity {entity_id}) does not exist") assert isinstance(entity, State) if attribute is None: value: Any = entity.state else: value = entity.attributes.get(attribute) if not isinstance(req_state, list): req_state = [req_state] is_state = False for req_state_value in req_state: state_value = req_state_value if (isinstance(req_state_value, str) and INPUT_ENTITY_ID.match(req_state_value) is not None): state_entity = hass.states.get(req_state_value) if not state_entity: 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: break if for_period is None or not is_state: return is_state return dt_util.utcnow() - for_period > entity.last_changed
def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" errors = [] all_ok = True for entity_id in entity_ids: entity_ok = False for zone_entity_id in zone_entity_ids: try: if zone(hass, zone_entity_id, entity_id): entity_ok = True 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 ConditionErrorContainer("zone", errors=errors) return all_ok
def zone( hass: HomeAssistant, zone_ent: None | str | State, entity: None | str | State, ) -> bool: """Test if zone-condition matches. Async friendly. """ if zone_ent is None: raise ConditionErrorMessage("zone", "no zone specified") if isinstance(zone_ent, str): zone_ent_id = zone_ent if (zone_ent := hass.states.get(zone_ent)) is None: raise ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}")
def async_numeric_state( # noqa: C901 hass: HomeAssistant, entity: None | str | State, below: float | str | None = None, above: float | str | None = None, value_template: Template | None = None, variables: TemplateVarsType = None, attribute: str | None = None, ) -> bool: """Test a numeric state condition.""" if entity is None: raise ConditionErrorMessage("numeric_state", "no entity specified") if isinstance(entity, str): entity_id = entity if (entity := hass.states.get(entity)) is None: raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}")
def state( hass: HomeAssistant, entity: None | str | State, req_state: Any, for_period: timedelta | None = None, attribute: str | None = None, ) -> bool: """Test if state matches requirements. Async friendly. """ if entity is None: raise ConditionErrorMessage("state", "no entity specified") if isinstance(entity, str): entity_id = entity if (entity := hass.states.get(entity)) is None: raise ConditionErrorMessage("state", f"unknown entity {entity_id}")
def async_template(hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None) -> bool: """Test if template condition matches.""" try: value: str = value_template.async_render(variables, parse_result=False) except TemplateError as ex: raise ConditionErrorMessage("template", str(ex)) from ex return value.lower() == "true"
def async_template( hass: HomeAssistant, value_template: Template, variables: TemplateVarsType = None, trace_result: bool = True, ) -> bool: """Test if template condition matches.""" try: info = value_template.async_render_to_info(variables, parse_result=False) value = info.result() except TemplateError as ex: raise ConditionErrorMessage("template", str(ex)) from ex result = value.lower() == "true" if trace_result: condition_trace_set_result(result, entities=list(info.entities)) return result
def time( hass: HomeAssistant, before: dt_time | str | None = None, after: dt_time | str | None = None, weekday: None | str | Container[str] = None, ) -> bool: """Test if local time condition matches. Handle the fact that time is continuous and we may be testing for a period that crosses midnight. In that case it is easier to test for the opposite. "(23:59 <= now < 00:01)" would be the same as "not (00:01 <= now < 23:59)". """ now = dt_util.now() now_time = now.time() if after is None: after = dt_time(0) elif isinstance(after, str): if not (after_entity := hass.states.get(after)): raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") if after_entity.domain == "input_datetime": after = dt_time( after_entity.attributes.get("hour", 23), after_entity.attributes.get("minute", 59), after_entity.attributes.get("second", 59), ) elif after_entity.attributes.get( ATTR_DEVICE_CLASS ) == SensorDeviceClass.TIMESTAMP and after_entity.state not in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): after_datetime = dt_util.parse_datetime(after_entity.state) if after_datetime is None: return False after = dt_util.as_local(after_datetime).time() else: return False
def zone( hass: HomeAssistant, zone_ent: None | str | State, entity: None | str | State, ) -> bool: """Test if zone-condition matches. Async friendly. """ if zone_ent is None: 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 ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}") if entity is None: raise ConditionErrorMessage("zone", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: raise ConditionErrorMessage("zone", f"unknown entity {entity_id}") else: entity_id = entity.entity_id latitude = entity.attributes.get(ATTR_LATITUDE) longitude = entity.attributes.get(ATTR_LONGITUDE) if latitude is None: raise ConditionErrorMessage( "zone", f"entity {entity_id} has no 'latitude' attribute" ) if longitude is None: 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) )
def async_numeric_state( # noqa: C901 hass: HomeAssistant, entity: None | str | State, below: float | str | None = None, above: float | str | None = None, value_template: Template | None = None, variables: TemplateVarsType = None, attribute: str | None = None, ) -> bool: """Test a numeric state condition.""" if entity is None: raise ConditionErrorMessage("numeric_state", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: 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 ConditionErrorMessage( "numeric_state", f"attribute '{attribute}' (of entity {entity_id}) does not exist", ) value: Any = None if value_template is None: if attribute is None: value = entity.state else: value = entity.attributes.get(attribute) else: variables = dict(variables or {}) variables["state"] = entity try: value = value_template.async_render(variables) except TemplateError as ex: raise ConditionErrorMessage( "numeric_state", f"template error: {ex}" ) from ex # Known states that never match the numeric condition if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): return False try: fvalue = float(value) 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: if isinstance(below, str): below_entity = hass.states.get(below) if not below_entity: raise ConditionErrorMessage( "numeric_state", f"unknown 'below' entity {below}" ) if below_entity.state in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): return False try: if fvalue >= float(below_entity.state): condition_trace_set_result( False, state=fvalue, wanted_state_below=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: condition_trace_set_result(False, state=fvalue, wanted_state_below=below) return False if above is not None: if isinstance(above, str): above_entity = hass.states.get(above) if not above_entity: raise ConditionErrorMessage( "numeric_state", f"unknown 'above' entity {above}" ) if above_entity.state in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): return False try: if fvalue <= float(above_entity.state): condition_trace_set_result( False, state=fvalue, wanted_state_above=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: condition_trace_set_result(False, state=fvalue, wanted_state_above=above) return False condition_trace_set_result(True, state=fvalue) return True
def time( hass: HomeAssistant, before: dt_util.dt.time | str | None = None, after: dt_util.dt.time | str | None = None, weekday: None | str | Container[str] = None, ) -> bool: """Test if local time condition matches. Handle the fact that time is continuous and we may be testing for a period that crosses midnight. In that case it is easier to test for the opposite. "(23:59 <= now < 00:01)" would be the same as "not (00:01 <= now < 23:59)". """ now = dt_util.now() now_time = now.time() if after is None: after = dt_util.dt.time(0) elif isinstance(after, str): after_entity = hass.states.get(after) if not after_entity: raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") if after_entity.domain == "input_datetime": after = dt_util.dt.time( after_entity.attributes.get("hour", 23), after_entity.attributes.get("minute", 59), after_entity.attributes.get("second", 59), ) elif after_entity.attributes.get( ATTR_DEVICE_CLASS ) == DEVICE_CLASS_TIMESTAMP and after_entity.state not in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): after_datetime = dt_util.parse_datetime(after_entity.state) if after_datetime is None: return False after = dt_util.as_local(after_datetime).time() else: return False if before is None: before = dt_util.dt.time(23, 59, 59, 999999) elif isinstance(before, str): before_entity = hass.states.get(before) if not before_entity: raise ConditionErrorMessage("time", f"unknown 'before' entity {before}") if before_entity.domain == "input_datetime": before = dt_util.dt.time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), before_entity.attributes.get("second", 59), ) elif before_entity.attributes.get( ATTR_DEVICE_CLASS ) == DEVICE_CLASS_TIMESTAMP and before_entity.state not in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): before_timedatime = dt_util.parse_datetime(before_entity.state) if before_timedatime is None: return False before = dt_util.as_local(before_timedatime).time() else: return False if after < before: condition_trace_update_result(after=after, now_time=now_time, before=before) if not after <= now_time < before: return False else: condition_trace_update_result(after=after, now_time=now_time, before=before) if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] condition_trace_update_result(weekday=weekday, now_weekday=now_weekday) if (isinstance(weekday, str) and weekday != now_weekday or now_weekday not in weekday): return False return True
) return False value: Any = None if value_template is None: if attribute is None: value = entity.state else: value = entity.attributes.get(attribute) else: variables = dict(variables or {}) variables["state"] = entity try: value = value_template.async_render(variables) except TemplateError as ex: raise ConditionErrorMessage("numeric_state", f"template error: {ex}") from ex # Known states or attribute values that never match the numeric condition if value in (None, STATE_UNAVAILABLE, STATE_UNKNOWN): condition_trace_set_result( False, message=f"value '{value}' is non-numeric and treated as False", ) return False try: fvalue = float(value) except (ValueError, TypeError) as ex: raise ConditionErrorMessage( "numeric_state", f"entity {entity_id} state '{value}' cannot be processed as a number",
def async_numeric_state( hass: HomeAssistant, entity: Union[None, str, State], below: Optional[Union[float, str]] = None, above: Optional[Union[float, str]] = None, value_template: Optional[Template] = None, variables: TemplateVarsType = None, attribute: Optional[str] = None, ) -> bool: """Test a numeric state condition.""" if entity is None: raise ConditionErrorMessage("numeric_state", "no entity specified") if isinstance(entity, str): entity_id = entity entity = hass.states.get(entity) if entity is None: 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 ConditionErrorMessage( "numeric_state", f"attribute '{attribute}' (of entity {entity_id}) does not exist", ) value: Any = None if value_template is None: if attribute is None: value = entity.state else: value = entity.attributes.get(attribute) else: variables = dict(variables or {}) variables["state"] = entity try: value = value_template.async_render(variables) except TemplateError as ex: raise ConditionErrorMessage("numeric_state", f"template error: {ex}") from ex if value in (STATE_UNAVAILABLE, STATE_UNKNOWN): raise ConditionErrorMessage("numeric_state", f"state of {entity_id} is unavailable") try: fvalue = float(value) 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: if isinstance(below, str): below_entity = hass.states.get(below) if not below_entity or below_entity.state in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): 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 if above is not None: if isinstance(above, str): above_entity = hass.states.get(above) if not above_entity or above_entity.state in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): 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 return True