2016-02-21 19:13:40 +00:00
|
|
|
"""Location helpers for Home Assistant."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
2016-02-21 19:13:40 +00:00
|
|
|
|
2021-04-20 15:40:41 +00:00
|
|
|
from collections.abc import Iterable
|
2020-07-03 18:28:44 +00:00
|
|
|
import logging
|
2016-08-07 23:26:35 +00:00
|
|
|
|
2016-02-21 19:13:40 +00:00
|
|
|
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
2021-03-27 11:55:24 +00:00
|
|
|
from homeassistant.core import HomeAssistant, State
|
2016-02-21 19:13:40 +00:00
|
|
|
from homeassistant.util import location as loc_util
|
|
|
|
|
2020-07-03 18:28:44 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2016-02-21 19:13:40 +00:00
|
|
|
|
2016-08-07 23:26:35 +00:00
|
|
|
def has_location(state: State) -> bool:
|
2016-11-18 22:35:08 +00:00
|
|
|
"""Test if state contains a valid location.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
return (
|
2021-01-30 21:33:53 +00:00
|
|
|
isinstance(state, State)
|
2019-07-31 20:08:31 +00:00
|
|
|
and isinstance(state.attributes.get(ATTR_LATITUDE), float)
|
2019-07-31 19:25:30 +00:00
|
|
|
and isinstance(state.attributes.get(ATTR_LONGITUDE), float)
|
|
|
|
)
|
2016-02-21 19:13:40 +00:00
|
|
|
|
|
|
|
|
2021-04-17 06:35:21 +00:00
|
|
|
def closest(latitude: float, longitude: float, states: Iterable[State]) -> State | None:
|
2016-11-18 22:35:08 +00:00
|
|
|
"""Return closest state to point.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2016-02-21 19:13:40 +00:00
|
|
|
with_location = [state for state in states if has_location(state)]
|
|
|
|
|
|
|
|
if not with_location:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return min(
|
|
|
|
with_location,
|
|
|
|
key=lambda state: loc_util.distance(
|
2018-10-28 19:12:52 +00:00
|
|
|
state.attributes.get(ATTR_LATITUDE),
|
|
|
|
state.attributes.get(ATTR_LONGITUDE),
|
2019-07-31 19:25:30 +00:00
|
|
|
latitude,
|
|
|
|
longitude,
|
2020-10-13 00:17:30 +00:00
|
|
|
)
|
|
|
|
or 0,
|
2016-02-21 19:13:40 +00:00
|
|
|
)
|
2020-07-03 18:28:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
def find_coordinates(
|
2022-02-09 09:43:20 +00:00
|
|
|
hass: HomeAssistant, name: str, recursion_history: list | None = None
|
2021-03-17 17:34:19 +00:00
|
|
|
) -> str | None:
|
2022-02-09 09:43:20 +00:00
|
|
|
"""Try to resolve the a location from a supplied name or entity_id.
|
|
|
|
|
2023-01-08 23:44:09 +00:00
|
|
|
Will recursively resolve an entity if pointed to by the state of the supplied
|
|
|
|
entity.
|
|
|
|
|
|
|
|
Returns coordinates in the form of '90.000,180.000', an address or
|
|
|
|
the state of the last resolved entity.
|
2022-02-09 09:43:20 +00:00
|
|
|
"""
|
|
|
|
# Check if a friendly name of a zone was supplied
|
|
|
|
if (zone_coords := resolve_zone(hass, name)) is not None:
|
|
|
|
return zone_coords
|
|
|
|
|
|
|
|
# Check if an entity_id was supplied.
|
|
|
|
if (entity_state := hass.states.get(name)) is None:
|
|
|
|
_LOGGER.debug("Unable to find entity %s", name)
|
|
|
|
return name
|
2020-07-03 18:28:44 +00:00
|
|
|
|
2022-02-09 09:43:20 +00:00
|
|
|
# Check if the entity_state has location attributes
|
2020-07-03 18:28:44 +00:00
|
|
|
if has_location(entity_state):
|
|
|
|
return _get_location_from_attributes(entity_state)
|
|
|
|
|
2022-02-09 09:43:20 +00:00
|
|
|
# Check if entity_state is a zone
|
2020-07-03 18:28:44 +00:00
|
|
|
zone_entity = hass.states.get(f"zone.{entity_state.state}")
|
2022-02-18 10:31:37 +00:00
|
|
|
if has_location(zone_entity): # type: ignore[arg-type]
|
2020-07-03 18:28:44 +00:00
|
|
|
_LOGGER.debug(
|
2023-01-08 23:44:09 +00:00
|
|
|
"%s is in %s, getting zone location",
|
|
|
|
name,
|
|
|
|
zone_entity.entity_id, # type: ignore[union-attr]
|
2020-07-03 18:28:44 +00:00
|
|
|
)
|
2022-02-18 10:31:37 +00:00
|
|
|
return _get_location_from_attributes(zone_entity) # type: ignore[arg-type]
|
2020-07-03 18:28:44 +00:00
|
|
|
|
2022-02-09 09:43:20 +00:00
|
|
|
# Check if entity_state is a friendly name of a zone
|
|
|
|
if (zone_coords := resolve_zone(hass, entity_state.state)) is not None:
|
|
|
|
return zone_coords
|
|
|
|
|
|
|
|
# Check if entity_state is an entity_id
|
2020-07-03 18:28:44 +00:00
|
|
|
if recursion_history is None:
|
|
|
|
recursion_history = []
|
2022-02-09 09:43:20 +00:00
|
|
|
recursion_history.append(name)
|
2020-07-03 18:28:44 +00:00
|
|
|
if entity_state.state in recursion_history:
|
|
|
|
_LOGGER.error(
|
2022-12-27 10:18:56 +00:00
|
|
|
(
|
|
|
|
"Circular reference detected while trying to find coordinates of an"
|
|
|
|
" entity. The state of %s has already been checked"
|
|
|
|
),
|
2020-07-03 18:28:44 +00:00
|
|
|
entity_state.state,
|
|
|
|
)
|
|
|
|
return None
|
|
|
|
_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.state)
|
|
|
|
return find_coordinates(hass, entity_state.state, recursion_history)
|
|
|
|
|
2023-01-08 23:44:09 +00:00
|
|
|
# Might be an address, coordinates or anything else.
|
|
|
|
# This has to be checked by the caller.
|
2022-02-09 09:43:20 +00:00
|
|
|
return entity_state.state
|
2020-11-05 05:06:19 +00:00
|
|
|
|
2022-02-09 09:43:20 +00:00
|
|
|
|
|
|
|
def resolve_zone(hass: HomeAssistant, zone_name: str) -> str | None:
|
2023-01-08 23:44:09 +00:00
|
|
|
"""Get a lat/long from a zones friendly_name.
|
|
|
|
|
|
|
|
None is returned if no zone is found by that friendly_name.
|
|
|
|
"""
|
2022-02-09 09:43:20 +00:00
|
|
|
states = hass.states.async_all("zone")
|
|
|
|
for state in states:
|
|
|
|
if state.name == zone_name:
|
|
|
|
return _get_location_from_attributes(state)
|
|
|
|
|
|
|
|
return None
|
2020-07-03 18:28:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _get_location_from_attributes(entity_state: State) -> str:
|
|
|
|
"""Get the lat/long string from an entities attributes."""
|
|
|
|
attr = entity_state.attributes
|
2021-04-09 16:58:27 +00:00
|
|
|
return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}"
|