Re-architect Tile integration with new pytile (#43071)

This commit is contained in:
Aaron Bach 2021-01-27 04:06:09 -07:00 committed by GitHub
parent c8ad06e58a
commit 3841f0e42d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 103 additions and 126 deletions

View file

@ -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()

View file

@ -4,5 +4,6 @@ import logging
DOMAIN = "tile"
DATA_COORDINATOR = "coordinator"
DATA_TILE = "tile"
LOGGER = logging.getLogger(__package__)

View file

@ -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()

View file

@ -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"]
}

View file

@ -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

View file

@ -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