diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 6b43761956e..aceed9aa7ee 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,26 +1,23 @@ """The Tile component.""" import asyncio from datetime import timedelta +from functools import partial from pytile import async_login from pytile.errors import SessionExpiredError, TileError -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_COORDINATOR, DOMAIN, LOGGER +from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER PLATFORMS = ["device_tracker"] DEVICE_TYPES = ["PHONE", "TILE"] -DEFAULT_ATTRIBUTION = "Data provided by Tile" -DEFAULT_ICON = "mdi:view-grid" +DEFAULT_INIT_TASK_LIMIT = 2 DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" @@ -28,108 +25,71 @@ CONF_SHOW_INACTIVE = "show_inactive" async def async_setup(hass, config): """Set up the Tile component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - + hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_TILE: {}} return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up Tile as config entry.""" + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} + websession = aiohttp_client.async_get_clientsession(hass) - client = await async_login( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], - session=websession, - ) + try: + client = await async_login( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + session=websession, + ) + hass.data[DOMAIN][DATA_TILE][entry.entry_id] = await client.async_get_tiles() + except TileError as err: + raise ConfigEntryNotReady("Error during integration setup") from err - async def async_update_data(): - """Get new data from the API.""" + async def async_update_tile(tile): + """Update the Tile.""" try: - return await client.tiles.all() + return await tile.async_update() except SessionExpiredError: LOGGER.info("Tile session expired; creating a new one") await client.async_init() except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=config_entry.title, - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=async_update_data, - ) + coordinator_init_tasks = [] + for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items(): + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + tile_uuid + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=tile.name, + update_interval=DEFAULT_UPDATE_INTERVAL, + update_method=partial(async_update_tile, tile), + ) + coordinator_init_tasks.append(coordinator.async_refresh()) - await coordinator.async_refresh() - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(entry, component) ) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a Tile config entry.""" unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) + hass.config_entries.async_forward_entry_unload(entry, component) for component in PLATFORMS ] ) ) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) return unload_ok - - -class TileEntity(CoordinatorEntity): - """Define a generic Tile entity.""" - - def __init__(self, coordinator): - """Initialize.""" - super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._name = None - self._unique_id = None - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return DEFAULT_ICON - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._unique_id - - @callback - def _handle_coordinator_update(self): - """Respond to a DataUpdateCoordinator update.""" - self._update_from_latest_data() - self.async_write_ha_state() - - @callback - def _update_from_latest_data(self): - """Update the entity from the latest data.""" - raise NotImplementedError - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await super().async_added_to_hass() - self._update_from_latest_data() diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index 91f5b838642..0f6f0dabb5c 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -4,5 +4,6 @@ import logging DOMAIN = "tile" DATA_COORDINATOR = "coordinator" +DATA_TILE = "tile" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 5b0065b2c4e..ae3852a2b07 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -4,10 +4,11 @@ import logging from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DATA_COORDINATOR, DOMAIN, TileEntity +from . import DATA_COORDINATOR, DATA_TILE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -19,17 +20,19 @@ ATTR_RING_STATE = "ring_state" ATTR_VOIP_STATE = "voip_state" ATTR_TILE_NAME = "tile_name" +DEFAULT_ATTRIBUTION = "Data provided by Tile" +DEFAULT_ICON = "mdi:view-grid" -async def async_setup_entry(hass, config_entry, async_add_entities): + +async def async_setup_entry(hass, entry, async_add_entities): """Set up Tile device trackers.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - async_add_entities( [ - TileDeviceTracker(coordinator, tile_uuid, tile) - for tile_uuid, tile in coordinator.data.items() - ], - True, + TileDeviceTracker( + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], tile + ) + for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items() + ] ) @@ -54,21 +57,19 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): return True -class TileDeviceTracker(TileEntity, TrackerEntity): +class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Representation of a network infrastructure device.""" - def __init__(self, coordinator, tile_uuid, tile): + def __init__(self, coordinator, tile): """Initialize.""" super().__init__(coordinator) - self._name = tile["name"] + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._tile = tile - self._tile_uuid = tile_uuid - self._unique_id = f"tile_{tile_uuid}" @property def available(self): """Return if entity is available.""" - return self.coordinator.last_update_success and not self._tile["is_dead"] + return self.coordinator.last_update_success and not self._tile.dead @property def battery_level(self): @@ -78,53 +79,68 @@ class TileDeviceTracker(TileEntity, TrackerEntity): """ return None + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return DEFAULT_ICON + @property def location_accuracy(self): """Return the location accuracy of the device. Value in meters. """ - state = self._tile["last_tile_state"] - h_accuracy = state.get("h_accuracy") - v_accuracy = state.get("v_accuracy") - - if h_accuracy is not None and v_accuracy is not None: - return round( - ( - self._tile["last_tile_state"]["h_accuracy"] - + self._tile["last_tile_state"]["v_accuracy"] - ) - / 2 - ) - - if h_accuracy is not None: - return h_accuracy - - if v_accuracy is not None: - return v_accuracy - - return None + return self._tile.accuracy @property def latitude(self) -> float: """Return latitude value of the device.""" - return self._tile["last_tile_state"]["latitude"] + return self._tile.latitude @property def longitude(self) -> float: """Return longitude value of the device.""" - return self._tile["last_tile_state"]["longitude"] + return self._tile.longitude + + @property + def name(self): + """Return the name.""" + return self._tile.name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"tile_{self._tile.uuid}" @property def source_type(self): """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS + @callback + def _handle_coordinator_update(self): + """Respond to a DataUpdateCoordinator update.""" + self._update_from_latest_data() + self.async_write_ha_state() + @callback def _update_from_latest_data(self): """Update the entity from the latest data.""" - self._tile = self.coordinator.data[self._tile_uuid] - self._attrs[ATTR_ALTITUDE] = self._tile["last_tile_state"]["altitude"] - self._attrs[ATTR_IS_LOST] = self._tile["last_tile_state"]["is_lost"] - self._attrs[ATTR_RING_STATE] = self._tile["last_tile_state"]["ring_state"] - self._attrs[ATTR_VOIP_STATE] = self._tile["last_tile_state"]["voip_state"] + self._attrs.update( + { + ATTR_ALTITUDE: self._tile.altitude, + ATTR_IS_LOST: self._tile.lost, + ATTR_RING_STATE: self._tile.ring_state, + ATTR_VOIP_STATE: self._tile.voip_state, + } + ) + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._update_from_latest_data() diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 0f2c84c12e1..854fc663ba2 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,6 +3,6 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==4.0.0"], + "requirements": ["pytile==5.1.0"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1b3b401e367..84daf58ccee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1846,7 +1846,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==4.0.0 +pytile==5.1.0 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67180ce1789..b3fef08aa67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,7 +918,7 @@ python-velbus==2.1.2 python_awair==0.2.1 # homeassistant.components.tile -pytile==4.0.0 +pytile==5.1.0 # homeassistant.components.traccar pytraccar==0.9.0