Refactor Tado to use OAuth in the DeviceTracker (#102610)
* Refactor to use TadoConnector in the DeviceTracker * Proposing myself as code owner to be notified of issues * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Fixing method names * Current progress, switching machines * Updating DeviceTracker to working prototype * Removing unnecessary callback * Adding dispatcher logic * Minor fine-tuning the intervals * Removing unnecessary debug log * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fix sorting * Retrieve devices from the Tado connector data * Asyncio feedback & dispatch generic mobile devices * Updating const * Fine-tuning unloading * Making add_tracked_entites callback * Adding unload over dispatcher_connect * Convert on_demand_update to callback * Removing now unused method * Merging method to on_demand_u * Adding create_issue to address repair * Updating with better translation * Converting to callback * Adding _attr_should_poll * Putting back the on_demand_update * Adding unique_id * Converting to TrackerEntity * Adding import step (review needed!) * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tado/device_tracker.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Typing and location_name * Changing to _attr_unique_id * Import improvement attempt * Property feedback * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Adding CONF_HOME_ID and task in get_scanner * Updating descriptions * Removing the create_task * Putting back PLATFORM_SCHEMA * Adding device_tracker * Adding get for HomeID * Get it better ;) * Retrieve HomeID from API * Add integration title in dialogs * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fixing homeID and strings.json * Delete request in strings * Update deprecation date * Adding test cases for import flow * Update tests/components/tado/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/tado/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/tado/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Removing none * Fixing test cases * Update homeassistant/components/tado/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Removing from context manager * Removing code owner * Re-adding code owner * Fix get scanner return value * Fix device tracker interface --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
0694ff8965
commit
4decc2bbfb
8 changed files with 382 additions and 119 deletions
|
@ -1299,8 +1299,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
||||
/tests/components/tado/ @michaelarnauts @chiefdragon
|
||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||
/tests/components/tado/ @michaelarnauts @chiefdragon @erwindouna
|
||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||
/tests/components/tag/ @balloob @dmulcahey
|
||||
/homeassistant/components/tailscale/ @frenck
|
||||
|
|
|
@ -26,9 +26,11 @@ from .const import (
|
|||
DOMAIN,
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
PRESET_AUTO,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||
TEMP_OFFSET,
|
||||
UPDATE_LISTENER,
|
||||
UPDATE_MOBILE_DEVICE_TRACK,
|
||||
UPDATE_TRACK,
|
||||
)
|
||||
|
||||
|
@ -38,12 +40,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.SENSOR,
|
||||
Platform.WATER_HEATER,
|
||||
]
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
@ -85,12 +89,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
update_mobile_devices = async_track_time_interval(
|
||||
hass,
|
||||
lambda now: tadoconnector.update_mobile_devices(),
|
||||
SCAN_MOBILE_DEVICE_INTERVAL,
|
||||
)
|
||||
|
||||
update_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA: tadoconnector,
|
||||
UPDATE_TRACK: update_track,
|
||||
UPDATE_MOBILE_DEVICE_TRACK: update_mobile_devices,
|
||||
UPDATE_LISTENER: update_listener,
|
||||
}
|
||||
|
||||
|
@ -127,6 +138,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_TRACK]()
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_MOBILE_DEVICE_TRACK]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
@ -151,6 +163,7 @@ class TadoConnector:
|
|||
self.devices = None
|
||||
self.data = {
|
||||
"device": {},
|
||||
"mobile_device": {},
|
||||
"weather": {},
|
||||
"geofence": {},
|
||||
"zone": {},
|
||||
|
@ -171,6 +184,10 @@ class TadoConnector:
|
|||
self.home_id = tado_home["id"]
|
||||
self.home_name = tado_home["name"]
|
||||
|
||||
def get_mobile_devices(self):
|
||||
"""Return the Tado mobile devices."""
|
||||
return self.tado.getMobileDevices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the registered zones."""
|
||||
|
@ -178,6 +195,27 @@ class TadoConnector:
|
|||
self.update_zones()
|
||||
self.update_home()
|
||||
|
||||
def update_mobile_devices(self) -> None:
|
||||
"""Update the mobile devices."""
|
||||
try:
|
||||
mobile_devices = self.get_mobile_devices()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to connect to Tado while updating mobile devices")
|
||||
return
|
||||
|
||||
for mobile_device in mobile_devices:
|
||||
self.data["mobile_device"][mobile_device["id"]] = mobile_device
|
||||
|
||||
_LOGGER.debug(
|
||||
"Dispatching update to %s mobile devices: %s",
|
||||
self.home_id,
|
||||
mobile_devices,
|
||||
)
|
||||
dispatcher_send(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
)
|
||||
|
||||
def update_devices(self):
|
||||
"""Update the device data from Tado."""
|
||||
try:
|
||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.data_entry_flow import FlowResult
|
|||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONF_HOME_ID,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DOMAIN,
|
||||
|
@ -110,6 +111,45 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
_LOGGER.debug("Importing Tado from configuration.yaml")
|
||||
username = import_config[CONF_USERNAME]
|
||||
password = import_config[CONF_PASSWORD]
|
||||
imported_home_id = import_config[CONF_HOME_ID]
|
||||
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_HOME_ID: imported_home_id,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
validate_result = await validate_input(
|
||||
self.hass,
|
||||
{
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
},
|
||||
)
|
||||
except exceptions.HomeAssistantError:
|
||||
return self.async_abort(reason="import_failed")
|
||||
|
||||
home_id = validate_result[UNIQUE_ID]
|
||||
await self.async_set_unique_id(home_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=import_config[CONF_USERNAME],
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_HOME_ID: home_id,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
|
|
@ -36,8 +36,10 @@ TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
|
|||
|
||||
# Configuration
|
||||
CONF_FALLBACK = "fallback"
|
||||
CONF_HOME_ID = "home_id"
|
||||
DATA = "data"
|
||||
UPDATE_TRACK = "update_track"
|
||||
UPDATE_MOBILE_DEVICE_TRACK = "update_mobile_device_track"
|
||||
|
||||
# Weather
|
||||
CONDITIONS_MAP = {
|
||||
|
@ -177,6 +179,7 @@ TADO_TO_HA_SWING_MODE_MAP = {
|
|||
DOMAIN = "tado"
|
||||
|
||||
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}"
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED = "tado_mobile_device_update_received"
|
||||
UNIQUE_ID = "unique_id"
|
||||
|
||||
DEFAULT_NAME = "Tado"
|
||||
|
|
|
@ -1,33 +1,31 @@
|
|||
"""Support for Tado Smart device trackers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN,
|
||||
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
|
||||
DeviceScanner,
|
||||
SourceType,
|
||||
TrackerEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_HOME, STATE_NOT_HOME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOME_ID = "home_id"
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
|
@ -37,113 +35,166 @@ PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend(
|
|||
)
|
||||
|
||||
|
||||
def get_scanner(hass: HomeAssistant, config: ConfigType) -> TadoDeviceScanner | None:
|
||||
"""Return a Tado scanner."""
|
||||
scanner = TadoDeviceScanner(hass, config[DOMAIN])
|
||||
return scanner if scanner.success_init else None
|
||||
async def async_get_scanner(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> DeviceScanner | None:
|
||||
"""Configure the Tado device scanner."""
|
||||
device_config = config["device_tracker"]
|
||||
import_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD],
|
||||
CONF_HOME_ID: device_config.get(CONF_HOME_ID),
|
||||
},
|
||||
)
|
||||
|
||||
translation_key = "deprecated_yaml_import_device_tracker"
|
||||
if import_result.get("type") == FlowResultType.ABORT:
|
||||
translation_key = "import_aborted"
|
||||
if import_result.get("reason") == "import_failed":
|
||||
translation_key = "import_failed"
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml_import_device_tracker",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
Device = namedtuple("Device", ["mac", "name"])
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Tado device scannery entity."""
|
||||
_LOGGER.debug("Setting up Tado device scanner entity")
|
||||
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||
tracked: set = set()
|
||||
|
||||
@callback
|
||||
def update_devices() -> None:
|
||||
"""Update the values of the devices."""
|
||||
add_tracked_entities(hass, tado, async_add_entities, tracked)
|
||||
|
||||
update_devices()
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
update_devices,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TadoDeviceScanner(DeviceScanner):
|
||||
"""Scanner for geofenced devices from Tado."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the scanner."""
|
||||
self.hass = hass
|
||||
self.last_results = []
|
||||
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
# The Tado device tracker can work with or without a home_id
|
||||
self.home_id = config[CONF_HOME_ID] if CONF_HOME_ID in config else None
|
||||
|
||||
# If there's a home_id, we need a different API URL
|
||||
if self.home_id is None:
|
||||
self.tadoapiurl = "https://my.tado.com/api/v2/me"
|
||||
else:
|
||||
self.tadoapiurl = "https://my.tado.com/api/v2/homes/{home_id}/mobileDevices"
|
||||
|
||||
# The API URL always needs a username and password
|
||||
self.tadoapiurl += "?username={username}&password={password}"
|
||||
|
||||
self.websession = None
|
||||
|
||||
self.success_init = asyncio.run_coroutine_threadsafe(
|
||||
self._async_update_info(), hass.loop
|
||||
).result()
|
||||
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
async def async_scan_devices(self):
|
||||
"""Scan for devices and return a list containing found device ids."""
|
||||
await self._async_update_info()
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
async def async_get_device_name(self, device):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [
|
||||
result.name for result in self.last_results if result.mac == device
|
||||
]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
async def _async_update_info(self):
|
||||
"""Query Tado for device marked as at home.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.debug("Requesting Tado")
|
||||
|
||||
if self.websession is None:
|
||||
self.websession = async_create_clientsession(
|
||||
self.hass, cookie_jar=aiohttp.CookieJar(unsafe=True)
|
||||
)
|
||||
|
||||
last_results = []
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
# Format the URL here, so we can log the template URL if
|
||||
# anything goes wrong without exposing username and password.
|
||||
url = self.tadoapiurl.format(
|
||||
home_id=self.home_id, username=self.username, password=self.password
|
||||
)
|
||||
|
||||
response = await self.websession.get(url)
|
||||
|
||||
if response.status != HTTPStatus.OK:
|
||||
_LOGGER.warning("Error %d on %s", response.status, self.tadoapiurl)
|
||||
return False
|
||||
|
||||
tado_json = await response.json()
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Cannot load Tado data")
|
||||
return False
|
||||
|
||||
# Without a home_id, we fetched an URL where the mobile devices can be
|
||||
# found under the mobileDevices key.
|
||||
if "mobileDevices" in tado_json:
|
||||
tado_json = tado_json["mobileDevices"]
|
||||
|
||||
# Find devices that have geofencing enabled, and are currently at home.
|
||||
for mobile_device in tado_json:
|
||||
if mobile_device.get("location") and mobile_device["location"]["atHome"]:
|
||||
device_id = mobile_device["id"]
|
||||
device_name = mobile_device["name"]
|
||||
last_results.append(Device(device_id, device_name))
|
||||
|
||||
self.last_results = last_results
|
||||
@callback
|
||||
def add_tracked_entities(
|
||||
hass: HomeAssistant,
|
||||
tado: Any,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
tracked: set[str],
|
||||
) -> None:
|
||||
"""Add new tracker entities from Tado."""
|
||||
_LOGGER.debug("Fetching Tado devices from API")
|
||||
new_tracked = []
|
||||
for device_key, device in tado.data["mobile_device"].items():
|
||||
if device_key in tracked:
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Tado presence query successful, %d device(s) at home",
|
||||
len(self.last_results),
|
||||
"Adding Tado device %s with deviceID %s", device["name"], device_key
|
||||
)
|
||||
new_tracked.append(TadoDeviceTrackerEntity(device_key, device["name"], tado))
|
||||
tracked.add(device_key)
|
||||
|
||||
async_add_entities(new_tracked)
|
||||
|
||||
|
||||
class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
"""A Tado Device Tracker entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
device_name: str,
|
||||
tado: Any,
|
||||
) -> None:
|
||||
"""Initialize a Tado Device Tracker entity."""
|
||||
super().__init__()
|
||||
self._attr_unique_id = device_id
|
||||
self._device_id = device_id
|
||||
self._device_name = device_name
|
||||
self._tado = tado
|
||||
self._active = False
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
|
||||
@callback
|
||||
def update_state(self) -> None:
|
||||
"""Update the Tado device."""
|
||||
_LOGGER.debug(
|
||||
"Updating Tado mobile device: %s (ID: %s)",
|
||||
self._device_name,
|
||||
self._device_id,
|
||||
)
|
||||
device = self._tado.data["mobile_device"][self._device_id]
|
||||
|
||||
self._active = False
|
||||
if device.get("location") is not None and device["location"]["atHome"]:
|
||||
_LOGGER.debug("Tado device %s is at home", device["name"])
|
||||
self._active = True
|
||||
else:
|
||||
_LOGGER.debug("Tado device %s is not at home", device["name"])
|
||||
|
||||
@callback
|
||||
def on_demand_update(self) -> None:
|
||||
"""Update state on demand."""
|
||||
self.update_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback."""
|
||||
_LOGGER.debug("Registering Tado device tracker entity")
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
self.on_demand_update,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
self.update_state()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the device."""
|
||||
return self._device_name
|
||||
|
||||
@property
|
||||
def location_name(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
return STATE_HOME if self._active else STATE_NOT_HOME
|
||||
|
||||
@property
|
||||
def latitude(self) -> None:
|
||||
"""Return latitude value of the device."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def longitude(self) -> None:
|
||||
"""Return longitude value of the device."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def source_type(self) -> SourceType:
|
||||
"""Return the source type."""
|
||||
return SourceType.GPS
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"domain": "tado",
|
||||
"name": "Tado",
|
||||
"codeowners": ["@michaelarnauts", "@chiefdragon"],
|
||||
"codeowners": ["@michaelarnauts", "@chiefdragon", "@erwindouna"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
|
|
@ -123,5 +123,19 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_device_tracker": {
|
||||
"title": "Tado YAML device tracker configuration imported",
|
||||
"description": "Configuring the Tado Device Tracker using YAML is being removed.\nRemove the YAML device tracker configuration and restart Home Assistant."
|
||||
},
|
||||
"import_aborted": {
|
||||
"title": "Import aborted",
|
||||
"description": "Configuring the Tado Device Tracker using YAML is being removed.\n The import was aborted, due to an existing config entry being the same as the data being imported in the YAML. Remove the YAML device tracker configuration and restart Home Assistant. Please use the UI to configure Tado."
|
||||
},
|
||||
"failed_to_import": {
|
||||
"title": "Failed to import",
|
||||
"description": "Failed to import the configuration for the Tado Device Tracker. Please use the UI to configure Tado. Don't forget to delete the YAML configuration."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -260,3 +260,120 @@ async def test_form_homekit(hass: HomeAssistant) -> None:
|
|||
),
|
||||
)
|
||||
assert result["type"] == "abort"
|
||||
|
||||
|
||||
async def test_import_step(hass: HomeAssistant) -> None:
|
||||
"""Test import step."""
|
||||
mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
return_value=mock_tado_api,
|
||||
), patch(
|
||||
"homeassistant.components.tado.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": "1",
|
||||
}
|
||||
assert mock_setup_entry.call_count == 1
|
||||
|
||||
|
||||
async def test_import_step_existing_entry(hass: HomeAssistant) -> None:
|
||||
"""Test import step with existing entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_setup_entry.call_count == 0
|
||||
|
||||
|
||||
async def test_import_step_validation_failed(hass: HomeAssistant) -> None:
|
||||
"""Test import step with validation failed."""
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
side_effect=RuntimeError,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "import_failed"
|
||||
|
||||
|
||||
async def test_import_step_unique_id_configured(hass: HomeAssistant) -> None:
|
||||
"""Test import step with unique ID already configured."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
unique_id="unique_id",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_setup_entry.call_count == 0
|
||||
|
|
Loading…
Add table
Reference in a new issue