From a1f7e5354faaeb7ee4bc319927e22e1d2ca8b65e Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 29 Jun 2020 22:09:44 +0200 Subject: [PATCH 1/6] add helpers.location.coordinates --- homeassistant/helpers/location.py | 65 +++++++++++++++++++++++++++++++ tests/helpers/test_location.py | 56 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 0070ba4ca6fd8..295a9b0b82d17 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -1,11 +1,18 @@ """Location helpers for Home Assistant.""" +import logging from typing import Optional, Sequence +import voluptuous as vol + from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import State +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location as loc_util +_LOGGER = logging.getLogger(__name__) + def has_location(state: State) -> bool: """Test if state contains a valid location. @@ -41,3 +48,61 @@ def closest( longitude, ), ) + + +def coordinates( + hass: HomeAssistantType, entity_id: str, recursion_history: Optional[list] = None +) -> Optional[str]: + """Get the location from the entity state or attributes.""" + entity = hass.states.get(entity_id) + + if entity is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if has_location(entity): + return _get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = hass.states.get(f"zone.{entity.state}") + if has_location(zone_entity): # type: ignore + _LOGGER.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id # type: ignore + ) + return _get_location_from_attributes(zone_entity) # type: ignore + + # Resolve nested entity + if recursion_history is None: + recursion_history = [] + recursion_history.append(entity_id) + if entity.state in recursion_history: + _LOGGER.error( + "Circular Reference detected. The state of %s has already been checked.", + entity.state, + ) + return None + _LOGGER.debug("Getting nested entity for state: %s", entity.state) + nested_entity = hass.states.get(entity.state) + if nested_entity is not None: + _LOGGER.debug("Resolving nested entity_id: %s", entity.state) + return coordinates(hass, entity.state, recursion_history) + + # Check if state is valid coordinate set + try: + cv.gps(entity.state.split(",")) + except vol.Invalid: + _LOGGER.error( + "The state of %s is not a valid set of coordinates: %s", + entity_id, + entity.state, + ) + return None + else: + return entity.state + + +def _get_location_from_attributes(entity: State) -> str: + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 7c99090b77e3c..39b3167b2437f 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -43,3 +43,59 @@ def test_closest_returns_closest(): state2 = State("light.test", "on", {ATTR_LATITUDE: 125.45, ATTR_LONGITUDE: 125.45}) assert state == location.closest(123.45, 123.45, [state, state2]) + + +async def test_coordinates_function_as_attributes(hass): + """Test coordinates function.""" + hass.states.async_set( + "test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943} + ) + assert location.coordinates(hass, "test.object") == "32.87336,-117.22943" + + +async def test_coordinates_function_as_state(hass): + """Test coordinates function.""" + hass.states.async_set("test.object", "32.87336,-117.22943") + assert location.coordinates(hass, "test.object") == "32.87336,-117.22943" + + +async def test_coordinates_function_device_tracker_in_zone(hass): + """Test coordinates function.""" + hass.states.async_set( + "zone.home", "zoning", {"latitude": 32.87336, "longitude": -117.22943}, + ) + hass.states.async_set("device_tracker.device", "home") + assert location.coordinates(hass, "device_tracker.device") == "32.87336,-117.22943" + + +async def test_coordinates_function_device_tracker_from_input_select(hass): + """Test coordinates function.""" + hass.states.async_set( + "input_select.select", + "device_tracker.device", + {"options": "device_tracker.device"}, + ) + hass.states.async_set("device_tracker.device", "32.87336,-117.22943") + assert location.coordinates(hass, "input_select.select") == "32.87336,-117.22943" + + +def test_coordinates_function_returns_none_on_recursion(hass): + """Test coordinates function.""" + hass.states.async_set( + "test.first", "test.second", + ) + hass.states.async_set("test.second", "test.first") + assert location.coordinates(hass, "test.first") is None + + +async def test_coordinates_function_returns_none_if_invalid_coord(hass): + """Test test_coordinates function.""" + hass.states.async_set( + "test.object", "abc", + ) + assert location.coordinates(hass, "test.object") is None + + +def test_coordinates_function_returns_none_if_invalid_input(hass): + """Test test_coordinates function.""" + assert location.coordinates(hass, "test.abc") is None From 299934f0143a8ad535f2ba4b5c74b5a325432502 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 30 Jun 2020 07:39:23 +0200 Subject: [PATCH 2/6] Rename func to find_coordinates --- homeassistant/helpers/location.py | 6 +++--- tests/helpers/test_location.py | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 295a9b0b82d17..1783221a91aa7 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -50,10 +50,10 @@ def closest( ) -def coordinates( +def find_coordinates( hass: HomeAssistantType, entity_id: str, recursion_history: Optional[list] = None ) -> Optional[str]: - """Get the location from the entity state or attributes.""" + """Find the gps coordinates of the entity in the form of '90.000,180.000'.""" entity = hass.states.get(entity_id) if entity is None: @@ -86,7 +86,7 @@ def coordinates( nested_entity = hass.states.get(entity.state) if nested_entity is not None: _LOGGER.debug("Resolving nested entity_id: %s", entity.state) - return coordinates(hass, entity.state, recursion_history) + return find_coordinates(hass, entity.state, recursion_history) # Check if state is valid coordinate set try: diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 39b3167b2437f..2f99d42616f70 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -50,13 +50,13 @@ async def test_coordinates_function_as_attributes(hass): hass.states.async_set( "test.object", "happy", {"latitude": 32.87336, "longitude": -117.22943} ) - assert location.coordinates(hass, "test.object") == "32.87336,-117.22943" + assert location.find_coordinates(hass, "test.object") == "32.87336,-117.22943" async def test_coordinates_function_as_state(hass): """Test coordinates function.""" hass.states.async_set("test.object", "32.87336,-117.22943") - assert location.coordinates(hass, "test.object") == "32.87336,-117.22943" + assert location.find_coordinates(hass, "test.object") == "32.87336,-117.22943" async def test_coordinates_function_device_tracker_in_zone(hass): @@ -65,7 +65,10 @@ async def test_coordinates_function_device_tracker_in_zone(hass): "zone.home", "zoning", {"latitude": 32.87336, "longitude": -117.22943}, ) hass.states.async_set("device_tracker.device", "home") - assert location.coordinates(hass, "device_tracker.device") == "32.87336,-117.22943" + assert ( + location.find_coordinates(hass, "device_tracker.device") + == "32.87336,-117.22943" + ) async def test_coordinates_function_device_tracker_from_input_select(hass): @@ -76,7 +79,9 @@ async def test_coordinates_function_device_tracker_from_input_select(hass): {"options": "device_tracker.device"}, ) hass.states.async_set("device_tracker.device", "32.87336,-117.22943") - assert location.coordinates(hass, "input_select.select") == "32.87336,-117.22943" + assert ( + location.find_coordinates(hass, "input_select.select") == "32.87336,-117.22943" + ) def test_coordinates_function_returns_none_on_recursion(hass): @@ -85,7 +90,7 @@ def test_coordinates_function_returns_none_on_recursion(hass): "test.first", "test.second", ) hass.states.async_set("test.second", "test.first") - assert location.coordinates(hass, "test.first") is None + assert location.find_coordinates(hass, "test.first") is None async def test_coordinates_function_returns_none_if_invalid_coord(hass): @@ -93,9 +98,9 @@ async def test_coordinates_function_returns_none_if_invalid_coord(hass): hass.states.async_set( "test.object", "abc", ) - assert location.coordinates(hass, "test.object") is None + assert location.find_coordinates(hass, "test.object") is None def test_coordinates_function_returns_none_if_invalid_input(hass): """Test test_coordinates function.""" - assert location.coordinates(hass, "test.abc") is None + assert location.find_coordinates(hass, "test.abc") is None From b8e688b806856f647ca1edcadbdc674f38b51c43 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 30 Jun 2020 07:44:43 +0200 Subject: [PATCH 3/6] More meaningful error message --- homeassistant/helpers/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 1783221a91aa7..fc0b4114788c8 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -78,7 +78,7 @@ def find_coordinates( recursion_history.append(entity_id) if entity.state in recursion_history: _LOGGER.error( - "Circular Reference detected. The state of %s has already been checked.", + "Circular reference detected while trying to find coordinates of an entity. The state of %s has already been checked.", entity.state, ) return None From 1d680f0d9f2fb00a1a690c0289304b8e54009d04 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 1 Jul 2020 17:48:27 +0200 Subject: [PATCH 4/6] rename entity to entity_state --- homeassistant/helpers/location.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index fc0b4114788c8..b1f4002fb37b8 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -54,18 +54,18 @@ def find_coordinates( hass: HomeAssistantType, entity_id: str, recursion_history: Optional[list] = None ) -> Optional[str]: """Find the gps coordinates of the entity in the form of '90.000,180.000'.""" - entity = hass.states.get(entity_id) + entity_state = hass.states.get(entity_id) - if entity is None: + if entity_state is None: _LOGGER.error("Unable to find entity %s", entity_id) return None # Check if the entity has location attributes - if has_location(entity): - return _get_location_from_attributes(entity) + if has_location(entity_state): + return _get_location_from_attributes(entity_state) # Check if device is in a zone - zone_entity = hass.states.get(f"zone.{entity.state}") + zone_entity = hass.states.get(f"zone.{entity_state.state}") if has_location(zone_entity): # type: ignore _LOGGER.debug( "%s is in %s, getting zone location", entity_id, zone_entity.entity_id # type: ignore @@ -76,33 +76,33 @@ def find_coordinates( if recursion_history is None: recursion_history = [] recursion_history.append(entity_id) - if entity.state in recursion_history: + if entity_state.state in recursion_history: _LOGGER.error( "Circular reference detected while trying to find coordinates of an entity. The state of %s has already been checked.", - entity.state, + entity_state.state, ) return None - _LOGGER.debug("Getting nested entity for state: %s", entity.state) - nested_entity = hass.states.get(entity.state) + _LOGGER.debug("Getting nested entity for state: %s", entity_state.state) + nested_entity = hass.states.get(entity_state.state) if nested_entity is not None: - _LOGGER.debug("Resolving nested entity_id: %s", entity.state) - return find_coordinates(hass, entity.state, recursion_history) + _LOGGER.debug("Resolving nested entity_id: %s", entity_state.state) + return find_coordinates(hass, entity_state.state, recursion_history) # Check if state is valid coordinate set try: - cv.gps(entity.state.split(",")) + cv.gps(entity_state.state.split(",")) except vol.Invalid: _LOGGER.error( "The state of %s is not a valid set of coordinates: %s", entity_id, - entity.state, + entity_state.state, ) return None else: - return entity.state + return entity_state.state -def _get_location_from_attributes(entity: State) -> str: +def _get_location_from_attributes(entity_state: State) -> str: """Get the lat/long string from an entities attributes.""" - attr = entity.attributes + attr = entity_state.attributes return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) From 3a75c9507ddb8c158ee1aad9a0bcc95fd60d2eb6 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 2 Jul 2020 10:21:14 +0200 Subject: [PATCH 5/6] More meaningful error message for no location --- homeassistant/helpers/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index b1f4002fb37b8..4f0dcbfda9fa8 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -93,7 +93,7 @@ def find_coordinates( cv.gps(entity_state.state.split(",")) except vol.Invalid: _LOGGER.error( - "The state of %s is not a valid set of coordinates: %s", + "Entity %s does not contain a location and does not point at an entity that does: %s", entity_id, entity_state.state, ) From aeefc69df6f783c55950ab264d645688ee9f8567 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 2 Jul 2020 11:08:22 +0200 Subject: [PATCH 6/6] Dont end log messages with period --- homeassistant/helpers/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index 4f0dcbfda9fa8..92af08bbd004a 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -78,7 +78,7 @@ def find_coordinates( recursion_history.append(entity_id) if entity_state.state in recursion_history: _LOGGER.error( - "Circular reference detected while trying to find coordinates of an entity. The state of %s has already been checked.", + "Circular reference detected while trying to find coordinates of an entity. The state of %s has already been checked", entity_state.state, ) return None