From e7e3e09063678e6154a1dc7cd83baae6c08bb576 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 19 Feb 2021 13:14:47 +0100 Subject: [PATCH] Raise ConditionError for zone errors (#46253) * Raise ConditionError for zone errors * Do not test missing state * Handle multiple entities/zones --- .../components/geo_location/trigger.py | 7 +- homeassistant/components/zone/trigger.py | 2 +- homeassistant/helpers/condition.py | 56 +++++++++---- tests/helpers/test_condition.py | 80 +++++++++++++++++++ 4 files changed, 127 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index aad281da117..9ca8c86e150 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -48,8 +48,11 @@ async def async_attach_trigger(hass, config, action, automation_info): return zone_state = hass.states.get(zone_entity_id) - from_match = condition.zone(hass, zone_state, from_state) - to_match = condition.zone(hass, zone_state, to_state) + + from_match = ( + condition.zone(hass, zone_state, from_state) if from_state else False + ) + to_match = condition.zone(hass, zone_state, to_state) if to_state else False if ( trigger_event == EVENT_ENTER diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index bc827c2ba0d..a5f89a7515d 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -58,7 +58,7 @@ async def async_attach_trigger( zone_state = hass.states.get(zone_entity_id) from_match = condition.zone(hass, zone_state, from_s) if from_s else False - to_match = condition.zone(hass, zone_state, to_s) + to_match = condition.zone(hass, zone_state, to_s) if to_s else False if ( event == EVENT_ENTER diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 2f63498c9cd..e09176dc098 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -588,23 +588,36 @@ def zone( Async friendly. """ + if zone_ent is None: + raise ConditionError("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: - return False - - if isinstance(entity, str): - entity = hass.states.get(entity) + if zone_ent is None: + raise ConditionError(f"Unknown zone {zone_ent_id}") if entity is None: - return False + raise ConditionError("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}") + else: + entity_id = entity.entity_id latitude = entity.attributes.get(ATTR_LATITUDE) longitude = entity.attributes.get(ATTR_LONGITUDE) - if latitude is None or longitude is None: - return False + if latitude is None: + raise ConditionError(f"Entity {entity_id} has no 'latitude' attribute") + + if longitude is None: + raise ConditionError(f"Entity {entity_id} has no 'longitude' attribute") return zone_cmp.in_zone( zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0) @@ -622,13 +635,26 @@ def zone_from_config( def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Test if condition.""" - return all( - any( - zone(hass, zone_entity_id, entity_id) - for zone_entity_id in zone_entity_ids - ) - for entity_id in entity_ids - ) + 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 ConditionError as ex: + errors.append(str(ex)) + + 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)) + + return all_ok return if_in_zone diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 1fc1e07da5d..0db8220bc50 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -811,6 +811,86 @@ async def test_numeric_state_using_input_number(hass): ) +async def test_zone_raises(hass): + """Test that zone raises ConditionError on errors.""" + test = await condition.async_from_config( + hass, + { + "condition": "zone", + "entity_id": "device_tracker.cat", + "zone": "zone.home", + }, + ) + + with pytest.raises(ConditionError, match="Unknown zone"): + test(hass) + + hass.states.async_set( + "zone.home", + "zoning", + {"name": "home", "latitude": 2.1, "longitude": 1.1, "radius": 10}, + ) + + with pytest.raises(ConditionError, match="Unknown entity"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat"}, + ) + + with pytest.raises(ConditionError, match="latitude"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat", "latitude": 2.1}, + ) + + with pytest.raises(ConditionError, match="longitude"): + test(hass) + + hass.states.async_set( + "device_tracker.cat", + "home", + {"friendly_name": "cat", "latitude": 2.1, "longitude": 1.1}, + ) + + # All okay, now test multiple failed conditions + assert test(hass) + + test = await condition.async_from_config( + hass, + { + "condition": "zone", + "entity_id": ["device_tracker.cat", "device_tracker.dog"], + "zone": ["zone.home", "zone.work"], + }, + ) + + with pytest.raises(ConditionError, match="dog"): + test(hass) + + with pytest.raises(ConditionError, match="work"): + test(hass) + + hass.states.async_set( + "zone.work", + "zoning", + {"name": "work", "latitude": 20, "longitude": 10, "radius": 25000}, + ) + + hass.states.async_set( + "device_tracker.dog", + "work", + {"friendly_name": "dog", "latitude": 20.1, "longitude": 10.1}, + ) + + assert test(hass) + + async def test_zone_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config(