core/homeassistant/helpers/location.py

128 lines
4.0 KiB
Python

"""Location helpers for Home Assistant."""
from __future__ import annotations
from collections.abc import Iterable
import logging
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import HomeAssistant, State
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.
Async friendly.
"""
return (
isinstance(state, State)
and isinstance(state.attributes.get(ATTR_LATITUDE), float)
and isinstance(state.attributes.get(ATTR_LONGITUDE), float)
)
def closest(latitude: float, longitude: float, states: Iterable[State]) -> State | None:
"""Return closest state to point.
Async friendly.
"""
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(
state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE),
latitude,
longitude,
)
or 0,
)
def find_coordinates(
hass: HomeAssistant, name: str, recursion_history: list | None = None
) -> str | None:
"""Try to resolve the a location from a supplied name or entity_id.
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.
"""
# 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
# Check if the entity_state has location attributes
if has_location(entity_state):
return _get_location_from_attributes(entity_state)
# Check if entity_state is a zone
zone_entity = hass.states.get(f"zone.{entity_state.state}")
if has_location(zone_entity): # type: ignore[arg-type]
_LOGGER.debug(
"%s is in %s, getting zone location",
name,
zone_entity.entity_id, # type: ignore[union-attr]
)
return _get_location_from_attributes(zone_entity) # type: ignore[arg-type]
# 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
if recursion_history is None:
recursion_history = []
recursion_history.append(name)
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.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)
# Might be an address, coordinates or anything else.
# This has to be checked by the caller.
return entity_state.state
def resolve_zone(hass: HomeAssistant, zone_name: str) -> str | None:
"""Get a lat/long from a zones friendly_name.
None is returned if no zone is found by that friendly_name.
"""
states = hass.states.async_all("zone")
for state in states:
if state.name == zone_name:
return _get_location_from_attributes(state)
return None
def _get_location_from_attributes(entity_state: State) -> str:
"""Get the lat/long string from an entities attributes."""
attr = entity_state.attributes
return f"{attr.get(ATTR_LATITUDE)},{attr.get(ATTR_LONGITUDE)}"