1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-02 05:34:15 +01:00
Files
core/homeassistant/components/mobile_app/device_tracker.py
T

224 lines
7.0 KiB
Python

"""Device tracker for Mobile app."""
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, Self
import voluptuous as vol
from homeassistant.components.device_tracker import (
ATTR_BATTERY,
ATTR_GPS,
ATTR_LOCATION_NAME,
TrackerEntity,
)
from homeassistant.components.zone import (
ENTITY_ID_FORMAT as ZONE_ENTITY_ID_FORMAT,
HOME_ZONE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_DEVICE_ID,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
STATE_HOME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
from .const import (
ATTR_ALTITUDE,
ATTR_COURSE,
ATTR_DEVICE_NAME,
ATTR_SPEED,
ATTR_VERTICAL_ACCURACY,
SIGNAL_LOCATION_UPDATE,
)
from .helpers import device_info
_LOGGER = logging.getLogger(__name__)
ATTR_KEYS = (ATTR_ALTITUDE, ATTR_COURSE, ATTR_SPEED, ATTR_VERTICAL_ACCURACY)
LOCATION_UPDATE_SCHEMA = vol.All(
cv.key_dependency(ATTR_GPS, ATTR_GPS_ACCURACY),
vol.Schema(
{
vol.Optional(ATTR_LOCATION_NAME): cv.string,
vol.Optional(ATTR_GPS): cv.gps,
vol.Optional(ATTR_GPS_ACCURACY): cv.positive_float,
vol.Optional(ATTR_BATTERY): cv.positive_int,
vol.Optional(ATTR_SPEED): cv.positive_int,
vol.Optional(ATTR_ALTITUDE): vol.Coerce(float),
vol.Optional(ATTR_COURSE): cv.positive_int,
vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int,
},
),
)
@dataclass
class MobileAppDeviceTrackerExtraStoredData(ExtraStoredData):
"""Object to hold mobile app device tracker data to be restored."""
data: dict[str, Any]
def as_dict(self) -> dict[str, Any]:
"""Return a dict representation of the stored data."""
return {"data": self.data}
@classmethod
def from_dict(cls, restored: dict[str, Any]) -> Self | None:
"""Initialize a stored mobile app entity data from a dict."""
if (data := restored.get("data")) is None:
return None
try:
validated = LOCATION_UPDATE_SCHEMA(data)
except vol.Invalid as err:
_LOGGER.debug("Discarding invalid restored device tracker data: %s", err)
return None
return cls(validated)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Mobile app based off an entry."""
entity = MobileAppEntity(entry)
async_add_entities([entity])
class MobileAppEntity(TrackerEntity, RestoreEntity):
"""Represent a tracked device."""
def __init__(self, entry: ConfigEntry) -> None:
"""Set up Mobile app entity."""
self._entry = entry
self._data: dict[str, Any] = {}
self._dispatch_unsub: Callable[[], None] | None = None
@property
def unique_id(self) -> str:
"""Return the unique ID."""
return self._entry.data[ATTR_DEVICE_ID]
@property
def battery_level(self) -> int | None:
"""Return the battery level of the device."""
return self._data.get(ATTR_BATTERY)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device specific attributes."""
attrs = {}
for key in ATTR_KEYS:
if (value := self._data.get(key)) is not None:
attrs[key] = value
return attrs
@property
def location_accuracy(self) -> float:
"""Return the gps accuracy of the device."""
return self._data.get(ATTR_GPS_ACCURACY, 0)
@property
def latitude(self) -> float | None:
"""Return latitude value of the device."""
if (gps := self._data.get(ATTR_GPS)) is None:
return None
return gps[0]
@property
def longitude(self) -> float | None:
"""Return longitude value of the device."""
if (gps := self._data.get(ATTR_GPS)) is None:
return None
return gps[1]
@property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device."""
if location_name := self._data.get(ATTR_LOCATION_NAME):
if location_name == HOME_ZONE:
return STATE_HOME
if zone_state := self.hass.states.get(
ZONE_ENTITY_ID_FORMAT.format(location_name)
):
return zone_state.name
return location_name
return None
@property
def name(self) -> str:
"""Return the name of the device."""
return self._entry.data[ATTR_DEVICE_NAME]
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return device_info(self._entry.data)
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to Home Assistant."""
await super().async_added_to_hass()
self._dispatch_unsub = async_dispatcher_connect(
self.hass,
SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id),
self.update_data,
)
if (extra_data := await self.async_get_last_extra_data()) is not None:
if (
restored := MobileAppDeviceTrackerExtraStoredData.from_dict(
extra_data.as_dict()
)
) is not None:
self._data = restored.data
return
# Fallback for entities saved before MobileAppDeviceTrackerExtraStoredData
# was introduced: reconstruct from the previous state's attributes.
# This can be removed in HA Core 2026.12.
if (state := await self.async_get_last_state()) is None:
return
attr = state.attributes
data = {
ATTR_GPS: (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)),
ATTR_GPS_ACCURACY: attr.get(ATTR_GPS_ACCURACY),
ATTR_BATTERY: attr.get(ATTR_BATTERY_LEVEL),
}
data.update({key: attr[key] for key in attr if key in ATTR_KEYS})
self._data = data
@property
def extra_restore_state_data(self) -> MobileAppDeviceTrackerExtraStoredData:
"""Return the entity data to be restored."""
return MobileAppDeviceTrackerExtraStoredData(self._data)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity is being removed from hass."""
await super().async_will_remove_from_hass()
if self._dispatch_unsub:
self._dispatch_unsub()
self._dispatch_unsub = None
@callback
def update_data(self, data: dict[str, Any]) -> None:
"""Mark the device as seen."""
self._data = data
self.async_write_ha_state()