mirror of
https://github.com/home-assistant/core.git
synced 2025-08-10 16:15:08 +02:00
Overhaul Tile
This commit is contained in:
@@ -1 +1,181 @@
|
|||||||
"""The tile component."""
|
"""The Tile component."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from pytile import async_login
|
||||||
|
from pytile.errors import TileError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ATTRIBUTION,
|
||||||
|
CONF_MONITORED_VARIABLES,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DATA_COORDINATOR, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
PLATFORMS = ["device_tracker"]
|
||||||
|
DEVICE_TYPES = ["PHONE", "TILE"]
|
||||||
|
|
||||||
|
DEFAULT_ATTRIBUTION = "Data provided by Tile"
|
||||||
|
DEFAULT_ICON = "mdi:view-grid"
|
||||||
|
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2)
|
||||||
|
|
||||||
|
CONF_SHOW_INACTIVE = "show_inactive"
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.All(
|
||||||
|
cv.deprecated(CONF_SHOW_INACTIVE, invalidation_version="0.114.0"),
|
||||||
|
cv.deprecated(CONF_MONITORED_VARIABLES, invalidation_version="0.114.0"),
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MONITORED_VARIABLES, default=DEVICE_TYPES
|
||||||
|
): vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up the Tile component."""
|
||||||
|
hass.data[DOMAIN] = {DATA_COORDINATOR: {}}
|
||||||
|
|
||||||
|
if DOMAIN not in config:
|
||||||
|
return True
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry):
|
||||||
|
"""Set up Tile as config entry."""
|
||||||
|
websession = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
|
||||||
|
client = await async_login(
|
||||||
|
config_entry.data[CONF_USERNAME],
|
||||||
|
config_entry.data[CONF_PASSWORD],
|
||||||
|
session=websession,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update_data():
|
||||||
|
"""Get new data from the API."""
|
||||||
|
try:
|
||||||
|
return await client.tiles.all()
|
||||||
|
except TileError as err:
|
||||||
|
raise UpdateFailed(f"Error while retrieving data: {err}")
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name=config_entry.title,
|
||||||
|
update_interval=DEFAULT_UPDATE_INTERVAL,
|
||||||
|
update_method=async_update_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, config_entry):
|
||||||
|
"""Unload a Tile config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class TileEntity(Entity):
|
||||||
|
"""Define a generic Tile entity."""
|
||||||
|
|
||||||
|
def __init__(self, coordinator):
|
||||||
|
"""Initialize."""
|
||||||
|
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
|
||||||
|
self._name = None
|
||||||
|
self._unique_id = None
|
||||||
|
self.coordinator = coordinator
|
||||||
|
|
||||||
|
@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 should_poll(self):
|
||||||
|
"""Disable polling."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return the unique ID of the entity."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_from_latest_data(self):
|
||||||
|
"""Update the entity from the latest data."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Register callbacks."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update():
|
||||||
|
"""Update the state."""
|
||||||
|
self._update_from_latest_data()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
self.async_on_remove(self.coordinator.async_add_listener(update))
|
||||||
|
|
||||||
|
self._update_from_latest_data()
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the entity.
|
||||||
|
|
||||||
|
Only used by the generic entity update service.
|
||||||
|
"""
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
52
homeassistant/components/tile/config_flow.py
Normal file
52
homeassistant/components/tile/config_flow.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"""Config flow to configure the Tile integration."""
|
||||||
|
from pytile import async_login
|
||||||
|
from pytile.errors import TileError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
|
||||||
|
class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a Tile config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
self.data_schema = vol.Schema(
|
||||||
|
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _show_form(self, errors=None):
|
||||||
|
"""Show the form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=self.data_schema, errors=errors or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config):
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
return await self.async_step_user(import_config)
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the start of the config flow."""
|
||||||
|
if not user_input:
|
||||||
|
return await self._show_form()
|
||||||
|
|
||||||
|
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await async_login(
|
||||||
|
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session
|
||||||
|
)
|
||||||
|
except TileError:
|
||||||
|
return await self._show_form({"base": "invalid_credentials"})
|
||||||
|
|
||||||
|
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)
|
8
homeassistant/components/tile/const.py
Normal file
8
homeassistant/components/tile/const.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""Define Tile constants."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
DOMAIN = "tile"
|
||||||
|
|
||||||
|
DATA_COORDINATOR = "coordinator"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
@@ -1,21 +1,9 @@
|
|||||||
"""Support for Tile® Bluetooth trackers."""
|
"""Support for Tile device trackers."""
|
||||||
from datetime import timedelta
|
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
||||||
import logging
|
from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from pytile import async_login
|
from . import DATA_COORDINATOR, DOMAIN, TileEntity
|
||||||
from pytile.errors import SessionExpiredError, TileError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
|
||||||
from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_PASSWORD, CONF_USERNAME
|
|
||||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
from homeassistant.util import slugify
|
|
||||||
from homeassistant.util.json import load_json, save_json
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
CLIENT_UUID_CONFIG_FILE = ".tile.conf"
|
|
||||||
DEVICE_TYPES = ["PHONE", "TILE"]
|
|
||||||
|
|
||||||
ATTR_ALTITUDE = "altitude"
|
ATTR_ALTITUDE = "altitude"
|
||||||
ATTR_CONNECTION_STATE = "connection_state"
|
ATTR_CONNECTION_STATE = "connection_state"
|
||||||
@@ -23,118 +11,92 @@ ATTR_IS_DEAD = "is_dead"
|
|||||||
ATTR_IS_LOST = "is_lost"
|
ATTR_IS_LOST = "is_lost"
|
||||||
ATTR_RING_STATE = "ring_state"
|
ATTR_RING_STATE = "ring_state"
|
||||||
ATTR_VOIP_STATE = "voip_state"
|
ATTR_VOIP_STATE = "voip_state"
|
||||||
ATTR_TILE_ID = "tile_identifier"
|
|
||||||
ATTR_TILE_NAME = "tile_name"
|
ATTR_TILE_NAME = "tile_name"
|
||||||
|
|
||||||
CONF_SHOW_INACTIVE = "show_inactive"
|
|
||||||
|
|
||||||
DEFAULT_ICON = "mdi:view-grid"
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=2)
|
"""Set up Tile device trackers."""
|
||||||
|
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
async_add_entities(
|
||||||
{
|
[
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
TileDeviceTracker(coordinator, tile_uuid, tile)
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
for tile_uuid, tile in coordinator.data.items()
|
||||||
vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
|
],
|
||||||
vol.Optional(CONF_MONITORED_VARIABLES, default=DEVICE_TYPES): vol.All(
|
True,
|
||||||
cv.ensure_list, [vol.In(DEVICE_TYPES)]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
|
||||||
"""Validate the configuration and return a Tile scanner."""
|
|
||||||
websession = aiohttp_client.async_get_clientsession(hass)
|
|
||||||
|
|
||||||
config_file = hass.config.path(
|
|
||||||
".{}{}".format(slugify(config[CONF_USERNAME]), CLIENT_UUID_CONFIG_FILE)
|
|
||||||
)
|
)
|
||||||
config_data = await hass.async_add_job(load_json, config_file)
|
|
||||||
if config_data:
|
|
||||||
client = await async_login(
|
|
||||||
config[CONF_USERNAME],
|
|
||||||
config[CONF_PASSWORD],
|
|
||||||
websession,
|
|
||||||
client_uuid=config_data["client_uuid"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
client = await async_login(
|
|
||||||
config[CONF_USERNAME], config[CONF_PASSWORD], websession
|
|
||||||
)
|
|
||||||
|
|
||||||
config_data = {"client_uuid": client.client_uuid}
|
|
||||||
await hass.async_add_job(save_json, config_file, config_data)
|
|
||||||
|
|
||||||
scanner = TileScanner(
|
|
||||||
client,
|
|
||||||
hass,
|
|
||||||
async_see,
|
|
||||||
config[CONF_MONITORED_VARIABLES],
|
|
||||||
config[CONF_SHOW_INACTIVE],
|
|
||||||
)
|
|
||||||
return await scanner.async_init()
|
|
||||||
|
|
||||||
|
|
||||||
class TileScanner:
|
class TileDeviceTracker(TileEntity, TrackerEntity):
|
||||||
"""Define an object to retrieve Tile data."""
|
"""Representation of a network infrastructure device."""
|
||||||
|
|
||||||
def __init__(self, client, hass, async_see, types, show_inactive):
|
def __init__(self, coordinator, tile_uuid, tile):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self._async_see = async_see
|
super().__init__(coordinator)
|
||||||
self._client = client
|
self._name = tile["name"]
|
||||||
self._hass = hass
|
self._tile = tile
|
||||||
self._show_inactive = show_inactive
|
self._tile_uuid = tile_uuid
|
||||||
self._types = types
|
self._unique_id = f"tile_{tile_uuid}"
|
||||||
|
|
||||||
async def async_init(self):
|
@property
|
||||||
"""Further initialize connection to the Tile servers."""
|
def available(self):
|
||||||
try:
|
"""Return if entity is available."""
|
||||||
await self._client.async_init()
|
return self.coordinator.last_update_success and not self._tile["is_dead"]
|
||||||
except TileError as err:
|
|
||||||
_LOGGER.error("Unable to set up Tile scanner: %s", err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
await self._async_update()
|
@property
|
||||||
|
def battery_level(self):
|
||||||
|
"""Return the battery level of the device.
|
||||||
|
|
||||||
async_track_time_interval(self._hass, self._async_update, DEFAULT_SCAN_INTERVAL)
|
Percentage from 0-100.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
return True
|
@property
|
||||||
|
def location_accuracy(self):
|
||||||
|
"""Return the location accuracy of the device.
|
||||||
|
|
||||||
async def _async_update(self, now=None):
|
Value in meters.
|
||||||
"""Update info from Tile."""
|
"""
|
||||||
try:
|
return round(
|
||||||
await self._client.async_init()
|
(
|
||||||
tiles = await self._client.tiles.all(
|
self._tile["last_tile_state"]["h_accuracy"]
|
||||||
whitelist=self._types, show_inactive=self._show_inactive
|
+ self._tile["last_tile_state"]["v_accuracy"]
|
||||||
)
|
)
|
||||||
except SessionExpiredError:
|
/ 2
|
||||||
_LOGGER.info("Session expired; trying again shortly")
|
)
|
||||||
return
|
|
||||||
except TileError as err:
|
|
||||||
_LOGGER.error("There was an error while updating: %s", err)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not tiles:
|
@property
|
||||||
_LOGGER.warning("No Tiles found")
|
def latitude(self) -> float:
|
||||||
return
|
"""Return latitude value of the device."""
|
||||||
|
return self._tile["last_tile_state"]["latitude"]
|
||||||
|
|
||||||
for tile in tiles:
|
@property
|
||||||
await self._async_see(
|
def longitude(self) -> float:
|
||||||
dev_id="tile_{}".format(slugify(tile["tile_uuid"])),
|
"""Return longitude value of the device."""
|
||||||
gps=(
|
return self._tile["last_tile_state"]["longitude"]
|
||||||
tile["last_tile_state"]["latitude"],
|
|
||||||
tile["last_tile_state"]["longitude"],
|
@property
|
||||||
),
|
def source_type(self):
|
||||||
attributes={
|
"""Return the source type, eg gps or router, of the device."""
|
||||||
ATTR_ALTITUDE: tile["last_tile_state"]["altitude"],
|
return SOURCE_TYPE_GPS
|
||||||
ATTR_CONNECTION_STATE: tile["last_tile_state"]["connection_state"],
|
|
||||||
ATTR_IS_DEAD: tile["is_dead"],
|
@property
|
||||||
ATTR_IS_LOST: tile["last_tile_state"]["is_lost"],
|
def state_attributes(self):
|
||||||
ATTR_RING_STATE: tile["last_tile_state"]["ring_state"],
|
"""Return the device state attributes."""
|
||||||
ATTR_VOIP_STATE: tile["last_tile_state"]["voip_state"],
|
attr = {}
|
||||||
ATTR_TILE_ID: tile["tile_uuid"],
|
attr.update(
|
||||||
ATTR_TILE_NAME: tile["name"],
|
super().state_attributes,
|
||||||
},
|
**{
|
||||||
icon=DEFAULT_ICON,
|
ATTR_ALTITUDE: self._tile["last_tile_state"]["altitude"],
|
||||||
)
|
ATTR_IS_LOST: self._tile["last_tile_state"]["is_lost"],
|
||||||
|
ATTR_RING_STATE: self._tile["last_tile_state"]["ring_state"],
|
||||||
|
ATTR_VOIP_STATE: self._tile["last_tile_state"]["voip_state"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return attr
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_from_latest_data(self):
|
||||||
|
"""Update the entity from the latest data."""
|
||||||
|
self._tile = self.coordinator.data[self._tile_uuid]
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"domain": "tile",
|
"domain": "tile",
|
||||||
"name": "Tile",
|
"name": "Tile",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tile",
|
"documentation": "https://www.home-assistant.io/integrations/tile",
|
||||||
"requirements": ["pytile==3.0.1"],
|
"requirements": ["pytile==3.0.6"],
|
||||||
"codeowners": ["@bachya"]
|
"codeowners": ["@bachya"]
|
||||||
}
|
}
|
||||||
|
29
homeassistant/components/tile/strings.json
Normal file
29
homeassistant/components/tile/strings.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Configure Tile",
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::email%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_credentials": "Invalid Tile credentials provided."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "This Tile account is already registered."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configure Tile",
|
||||||
|
"data": {
|
||||||
|
"show_inactive": "Show inactive Tiles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
homeassistant/components/tile/translations/en.json
Normal file
29
homeassistant/components/tile/translations/en.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "This Tile account is already registered."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"invalid_credentials": "Invalid Tile credentials provided."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "[%key:common::config_flow::data::email%]"
|
||||||
|
},
|
||||||
|
"title": "Configure Tile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"show_inactive": "Show inactive Tiles"
|
||||||
|
},
|
||||||
|
"title": "Configure Tile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -143,6 +143,7 @@ FLOWS = [
|
|||||||
"tellduslive",
|
"tellduslive",
|
||||||
"tesla",
|
"tesla",
|
||||||
"tibber",
|
"tibber",
|
||||||
|
"tile",
|
||||||
"toon",
|
"toon",
|
||||||
"totalconnect",
|
"totalconnect",
|
||||||
"tplink",
|
"tplink",
|
||||||
|
@@ -1765,7 +1765,7 @@ python_opendata_transport==0.2.1
|
|||||||
pythonegardia==1.0.40
|
pythonegardia==1.0.40
|
||||||
|
|
||||||
# homeassistant.components.tile
|
# homeassistant.components.tile
|
||||||
pytile==3.0.1
|
pytile==3.0.6
|
||||||
|
|
||||||
# homeassistant.components.touchline
|
# homeassistant.components.touchline
|
||||||
pytouchline==0.7
|
pytouchline==0.7
|
||||||
|
@@ -731,6 +731,9 @@ python-velbus==2.0.43
|
|||||||
# homeassistant.components.awair
|
# homeassistant.components.awair
|
||||||
python_awair==0.0.4
|
python_awair==0.0.4
|
||||||
|
|
||||||
|
# homeassistant.components.tile
|
||||||
|
pytile==3.0.6
|
||||||
|
|
||||||
# homeassistant.components.traccar
|
# homeassistant.components.traccar
|
||||||
pytraccar==0.9.0
|
pytraccar==0.9.0
|
||||||
|
|
||||||
|
1
tests/components/tile/__init__.py
Normal file
1
tests/components/tile/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Define tests for the Tile component."""
|
268
tests/components/tile/test_config_flow.py
Normal file
268
tests/components/tile/test_config_flow.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"""Define tests for the Tile config flow."""
|
||||||
|
from pyairvisual.errors import InvalidKeyError, NodeProError
|
||||||
|
|
||||||
|
from homeassistant import data_entry_flow
|
||||||
|
from homeassistant.components.airvisual import (
|
||||||
|
CONF_GEOGRAPHIES,
|
||||||
|
CONF_INTEGRATION_TYPE,
|
||||||
|
DOMAIN,
|
||||||
|
INTEGRATION_TYPE_GEOGRAPHY,
|
||||||
|
INTEGRATION_TYPE_NODE_PRO,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_IP_ADDRESS,
|
||||||
|
CONF_LATITUDE,
|
||||||
|
CONF_LONGITUDE,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SHOW_ON_MAP,
|
||||||
|
)
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_error(hass):
|
||||||
|
"""Test that errors are shown when duplicate entries are added."""
|
||||||
|
geography_conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
}
|
||||||
|
node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "12345"}
|
||||||
|
|
||||||
|
MockConfigEntry(
|
||||||
|
domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
MockConfigEntry(
|
||||||
|
domain=DOMAIN, unique_id="192.168.1.100", data=node_pro_conf
|
||||||
|
).add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data={"type": "Tile Node/Pro"}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=node_pro_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_identifier(hass):
|
||||||
|
"""Test that an invalid API key or Node/Pro ID throws an error."""
|
||||||
|
geography_conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"pyairvisual.api.API.nearest_city", side_effect=InvalidKeyError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_node_pro_error(hass):
|
||||||
|
"""Test that an invalid Node/Pro ID shows an error."""
|
||||||
|
node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"pyairvisual.node.Node.from_samba", side_effect=NodeProError,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data={"type": "Tile Node/Pro"}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=node_pro_conf
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {CONF_IP_ADDRESS: "unable_to_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration(hass):
|
||||||
|
"""Test migrating from version 1 to the current version."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_GEOGRAPHIES: [
|
||||||
|
{CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765},
|
||||||
|
{CONF_LATITUDE: 35.48847, CONF_LONGITUDE: 137.5263065},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, version=1, unique_id="abcde12345", data=conf
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
|
with patch("pyairvisual.api.API.nearest_city"):
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf})
|
||||||
|
|
||||||
|
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
|
||||||
|
assert len(config_entries) == 2
|
||||||
|
|
||||||
|
assert config_entries[0].unique_id == "51.528308, -0.3817765"
|
||||||
|
assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)"
|
||||||
|
assert config_entries[0].data == {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert config_entries[1].unique_id == "35.48847, 137.5263065"
|
||||||
|
assert config_entries[1].title == "Cloud API (35.48847, 137.5263065)"
|
||||||
|
assert config_entries[1].data == {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 35.48847,
|
||||||
|
CONF_LONGITUDE: 137.5263065,
|
||||||
|
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow(hass):
|
||||||
|
"""Test config flow options."""
|
||||||
|
geography_conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
}
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id="51.528308, -0.3817765",
|
||||||
|
data=geography_conf,
|
||||||
|
options={CONF_SHOW_ON_MAP: True},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.airvisual.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_SHOW_ON_MAP: False}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert config_entry.options == {CONF_SHOW_ON_MAP: False}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_geography(hass):
|
||||||
|
"""Test the geograph (cloud API) step."""
|
||||||
|
conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.airvisual.async_setup_entry", return_value=True
|
||||||
|
), patch("pyairvisual.api.API.nearest_city"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "Cloud API (51.528308, -0.3817765)"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_node_pro(hass):
|
||||||
|
"""Test the Node/Pro step."""
|
||||||
|
conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.airvisual.async_setup_entry", return_value=True
|
||||||
|
), patch("pyairvisual.node.Node.from_samba"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}, data={"type": "Tile Node/Pro"}
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input=conf
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "Node/Pro (192.168.1.100)"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_IP_ADDRESS: "192.168.1.100",
|
||||||
|
CONF_PASSWORD: "my_password",
|
||||||
|
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_import(hass):
|
||||||
|
"""Test the import step for both types of configuration."""
|
||||||
|
geography_conf = {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.airvisual.async_setup_entry", return_value=True
|
||||||
|
), patch("pyairvisual.api.API.nearest_city"):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "Cloud API (51.528308, -0.3817765)"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_LATITUDE: 51.528308,
|
||||||
|
CONF_LONGITUDE: -0.3817765,
|
||||||
|
CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_user(hass):
|
||||||
|
"""Test the user ("pick the integration type") step."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={"type": INTEGRATION_TYPE_GEOGRAPHY},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "geography"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={"type": INTEGRATION_TYPE_NODE_PRO},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "node_pro"
|
Reference in New Issue
Block a user