mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 17:27:10 +00:00
Tado migrate to OAuth Device Flow (#140761)
* Bump PyTado 0.19.0 * Initial setup * Current state * Update to PyTado 0.18.8 * First concept for review * Fix * Fix * Fix * First concept for review * Bump PyTado to 0.18.9 * Remove redundant part * Initial test setup * Authentication exceptions * Fix * Fix * Fix * Update version to 2 * All migration code * Small tuning * Add reauth unique ID check * Add reauth test * 100% on config flow * Making tests working on new device flow * Fix * Fix * Fix * Update homeassistant/components/tado/strings.json * Update homeassistant/components/tado/strings.json --------- Co-authored-by: Joostlek <joostlek@outlook.com> Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
parent
83a0ed4250
commit
358f78c7cd
@ -10,12 +10,17 @@ from PyTado.interface import Tado
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
@ -56,23 +61,34 @@ type TadoConfigEntry = ConfigEntry[TadoData]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool:
|
||||
"""Set up Tado from a config entry."""
|
||||
if CONF_REFRESH_TOKEN not in entry.data:
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
_async_import_options_from_data_if_missing(hass, entry)
|
||||
|
||||
_LOGGER.debug("Setting up Tado connection")
|
||||
_LOGGER.debug(
|
||||
"Creating tado instance with refresh token: %s",
|
||||
entry.data[CONF_REFRESH_TOKEN],
|
||||
)
|
||||
|
||||
def create_tado_instance() -> tuple[Tado, str]:
|
||||
"""Create a Tado instance, this time with a previously obtained refresh token."""
|
||||
tado = Tado(saved_refresh_token=entry.data[CONF_REFRESH_TOKEN])
|
||||
return tado, tado.device_activation_status()
|
||||
|
||||
try:
|
||||
tado = await hass.async_add_executor_job(
|
||||
Tado,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
tado, device_status = await hass.async_add_executor_job(create_tado_instance)
|
||||
except PyTado.exceptions.TadoWrongCredentialsException as err:
|
||||
raise ConfigEntryError(f"Invalid Tado credentials. Error: {err}") from err
|
||||
except PyTado.exceptions.TadoException as err:
|
||||
raise ConfigEntryNotReady(f"Error during Tado setup: {err}") from err
|
||||
_LOGGER.debug(
|
||||
"Tado connection established for username: %s", entry.data[CONF_USERNAME]
|
||||
)
|
||||
if device_status != "COMPLETED":
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Device login flow status is {device_status}. Starting re-authentication."
|
||||
)
|
||||
|
||||
_LOGGER.debug("Tado connection established")
|
||||
|
||||
coordinator = TadoDataUpdateCoordinator(hass, entry, tado)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@ -82,11 +98,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool
|
||||
|
||||
entry.runtime_data = TadoData(coordinator, mobile_coordinator)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if entry.version < 2:
|
||||
_LOGGER.debug("Migrating Tado entry to version 2. Current data: %s", entry.data)
|
||||
data = dict(entry.data)
|
||||
data.pop(CONF_USERNAME, None)
|
||||
data.pop(CONF_PASSWORD, None)
|
||||
hass.config_entries.async_update_entry(entry=entry, data=data, version=2)
|
||||
_LOGGER.debug("Migration to version 2 successful")
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def _async_import_options_from_data_if_missing(
|
||||
hass: HomeAssistant, entry: TadoConfigEntry
|
||||
@ -106,11 +134,6 @@ def _async_import_options_from_data_if_missing(
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: TadoConfigEntry):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@ -2,22 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import PyTado
|
||||
from PyTado.exceptions import TadoException
|
||||
from PyTado.http import DeviceActivationStatus
|
||||
from PyTado.interface import Tado
|
||||
import requests.exceptions
|
||||
import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.service_info.zeroconf import (
|
||||
ATTR_PROPERTIES_ID,
|
||||
@ -26,137 +29,149 @@ from homeassistant.helpers.service_info.zeroconf import (
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DOMAIN,
|
||||
UNIQUE_ID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
try:
|
||||
tado = await hass.async_add_executor_job(
|
||||
Tado, data[CONF_USERNAME], data[CONF_PASSWORD]
|
||||
)
|
||||
tado_me = await hass.async_add_executor_job(tado.get_me)
|
||||
except KeyError as ex:
|
||||
raise InvalidAuth from ex
|
||||
except RuntimeError as ex:
|
||||
raise CannotConnect from ex
|
||||
except requests.exceptions.HTTPError as ex:
|
||||
if ex.response.status_code > 400 and ex.response.status_code < 500:
|
||||
raise InvalidAuth from ex
|
||||
raise CannotConnect from ex
|
||||
|
||||
if "homes" not in tado_me or len(tado_me["homes"]) == 0:
|
||||
raise NoHomes
|
||||
|
||||
home = tado_me["homes"][0]
|
||||
unique_id = str(home["id"])
|
||||
name = home["name"]
|
||||
|
||||
return {"title": name, UNIQUE_ID: unique_id}
|
||||
|
||||
|
||||
class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Tado."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
login_task: asyncio.Task | None = None
|
||||
refresh_token: str | None = None
|
||||
tado: Tado | None = None
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth on credential failure."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Prepare reauth."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
"""Handle users reauth credentials."""
|
||||
|
||||
if self.tado is None:
|
||||
_LOGGER.debug("Initiating device activation")
|
||||
try:
|
||||
validated = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except NoHomes:
|
||||
errors["base"] = "no_homes"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
self.tado = await self.hass.async_add_executor_job(Tado)
|
||||
except TadoException:
|
||||
_LOGGER.exception("Error while initiating Tado")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
assert self.tado is not None
|
||||
tado_device_url = self.tado.device_verification_url()
|
||||
user_code = URL(tado_device_url).query["user_code"]
|
||||
|
||||
if "base" not in errors:
|
||||
await self.async_set_unique_id(validated[UNIQUE_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=validated["title"], data=user_input
|
||||
)
|
||||
async def _wait_for_login() -> None:
|
||||
"""Wait for the user to login."""
|
||||
assert self.tado is not None
|
||||
_LOGGER.debug("Waiting for device activation")
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.tado.device_activation)
|
||||
except Exception as ex:
|
||||
_LOGGER.exception("Error while waiting for device activation")
|
||||
raise CannotConnect from ex
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
if (
|
||||
self.tado.device_activation_status()
|
||||
is not DeviceActivationStatus.COMPLETED
|
||||
):
|
||||
raise CannotConnect
|
||||
|
||||
_LOGGER.debug("Checking login task")
|
||||
if self.login_task is None:
|
||||
_LOGGER.debug("Creating task for device activation")
|
||||
self.login_task = self.hass.async_create_task(_wait_for_login())
|
||||
|
||||
if self.login_task.done():
|
||||
_LOGGER.debug("Login task is done, checking results")
|
||||
if self.login_task.exception():
|
||||
return self.async_show_progress_done(next_step_id="timeout")
|
||||
self.refresh_token = await self.hass.async_add_executor_job(
|
||||
self.tado.get_refresh_token
|
||||
)
|
||||
return self.async_show_progress_done(next_step_id="finish_login")
|
||||
|
||||
return self.async_show_progress(
|
||||
step_id="user",
|
||||
progress_action="wait_for_device",
|
||||
description_placeholders={
|
||||
"url": tado_device_url,
|
||||
"code": user_code,
|
||||
},
|
||||
progress_task=self.login_task,
|
||||
)
|
||||
|
||||
async def async_step_finish_login(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the finalization of reauth."""
|
||||
_LOGGER.debug("Finalizing reauth")
|
||||
assert self.tado is not None
|
||||
tado_me = await self.hass.async_add_executor_job(self.tado.get_me)
|
||||
|
||||
if "homes" not in tado_me or len(tado_me["homes"]) == 0:
|
||||
return self.async_abort(reason="no_homes")
|
||||
|
||||
home = tado_me["homes"][0]
|
||||
unique_id = str(home["id"])
|
||||
name = home["name"]
|
||||
|
||||
if self.source != SOURCE_REAUTH:
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={CONF_REFRESH_TOKEN: self.refresh_token},
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_mismatch(reason="reauth_account_mismatch")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data={CONF_REFRESH_TOKEN: self.refresh_token},
|
||||
)
|
||||
|
||||
async def async_step_timeout(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle issues that need transition await from progress step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="timeout",
|
||||
)
|
||||
del self.login_task
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_homekit(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle HomeKit discovery."""
|
||||
self._async_abort_entries_match()
|
||||
properties = {
|
||||
key.lower(): value for (key, value) in discovery_info.properties.items()
|
||||
key.lower(): value for key, value in discovery_info.properties.items()
|
||||
}
|
||||
await self.async_set_unique_id(properties[ATTR_PROPERTIES_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reconfiguration flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
user_input[CONF_USERNAME] = reconfigure_entry.data[CONF_USERNAME]
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except PyTado.exceptions.TadoWrongCredentialsException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except NoHomes:
|
||||
errors["base"] = "no_homes"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry, data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@ -173,8 +188,10 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(data=user_input)
|
||||
if user_input:
|
||||
result = self.async_create_entry(data=user_input)
|
||||
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
return result
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
@ -191,11 +208,3 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class NoHomes(HomeAssistantError):
|
||||
"""Error to indicate the account has no homes."""
|
||||
|
@ -37,6 +37,7 @@ TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
|
||||
# Configuration
|
||||
CONF_FALLBACK = "fallback"
|
||||
CONF_HOME_ID = "home_id"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
DATA = "data"
|
||||
|
||||
# Weather
|
||||
|
@ -10,7 +10,6 @@ from PyTado.interface import Tado
|
||||
from requests import RequestException
|
||||
|
||||
from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@ -20,6 +19,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
DOMAIN,
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
@ -58,8 +58,7 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self._tado = tado
|
||||
self._username = config_entry.data[CONF_USERNAME]
|
||||
self._password = config_entry.data[CONF_PASSWORD]
|
||||
self._refresh_token = config_entry.data[CONF_REFRESH_TOKEN]
|
||||
self._fallback = config_entry.options.get(
|
||||
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
|
||||
)
|
||||
@ -108,6 +107,18 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
self.data["weather"] = home["weather"]
|
||||
self.data["geofence"] = home["geofence"]
|
||||
|
||||
refresh_token = await self.hass.async_add_executor_job(
|
||||
self._tado.get_refresh_token
|
||||
)
|
||||
|
||||
if refresh_token != self._refresh_token:
|
||||
_LOGGER.debug("New refresh token obtained from Tado: %s", refresh_token)
|
||||
self._refresh_token = refresh_token
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry,
|
||||
data={**self.config_entry.data, CONF_REFRESH_TOKEN: refresh_token},
|
||||
)
|
||||
|
||||
return self.data
|
||||
|
||||
async def _async_update_devices(self) -> dict[str, dict]:
|
||||
|
@ -14,5 +14,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["PyTado"],
|
||||
"requirements": ["python-tado==0.18.6"]
|
||||
"requirements": ["python-tado==0.18.9"]
|
||||
}
|
||||
|
@ -1,33 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"progress": {
|
||||
"wait_for_device": "To authenticate, open the following URL and login at Tado:\n{url}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{code}```\n\n\nThe login attempt will time out after five minutes."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"could_not_authenticate": "Could not authenticate with Tado.",
|
||||
"no_homes": "There are no homes linked to this Tado account.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "Connect to your Tado account"
|
||||
"reauth_confirm": {
|
||||
"title": "Authenticate with Tado",
|
||||
"description": "You need to reauthenticate with Tado. Press `Submit` to start the authentication process."
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure your Tado",
|
||||
"description": "Reconfigure the entry for your account: `{username}`.",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "Enter the (new) password for Tado."
|
||||
}
|
||||
"timeout": {
|
||||
"description": "The authentication process timed out. Please try again."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"no_homes": "There are no homes linked to this Tado account.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -2477,7 +2477,7 @@ python-snoo==0.6.4
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.6
|
||||
python-tado==0.18.9
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -2007,7 +2007,7 @@ python-snoo==0.6.4
|
||||
python-songpal==0.16.2
|
||||
|
||||
# homeassistant.components.tado
|
||||
python-tado==0.18.6
|
||||
python-tado==0.18.9
|
||||
|
||||
# homeassistant.components.technove
|
||||
python-technove==2.0.0
|
||||
|
@ -1 +1 @@
|
||||
"""Tests for the tado integration."""
|
||||
"""Tests for the Tado integration."""
|
||||
|
50
tests/components/tado/conftest.py
Normal file
50
tests/components/tado/conftest.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""Fixtures for Tado tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from PyTado.http import DeviceActivationStatus
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tado import CONF_REFRESH_TOKEN, DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tado_api() -> Generator[MagicMock]:
|
||||
"""Mock the Tado API."""
|
||||
with (
|
||||
patch("homeassistant.components.tado.Tado") as mock_tado,
|
||||
patch("homeassistant.components.tado.config_flow.Tado", new=mock_tado),
|
||||
):
|
||||
client = mock_tado.return_value
|
||||
client.device_verification_url.return_value = (
|
||||
"https://login.tado.com/oauth2/device?user_code=TEST"
|
||||
)
|
||||
client.device_activation_status.return_value = DeviceActivationStatus.COMPLETED
|
||||
client.get_me.return_value = load_json_object_fixture("me.json", DOMAIN)
|
||||
client.get_refresh_token.return_value = "refresh"
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Mock the setup entry."""
|
||||
with patch(
|
||||
"homeassistant.components.tado.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_REFRESH_TOKEN: "refresh",
|
||||
},
|
||||
unique_id="1",
|
||||
version=2,
|
||||
)
|
8
tests/components/tado/fixtures/device_authorize.json
Normal file
8
tests/components/tado/fixtures/device_authorize.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"device_code": "ABCD",
|
||||
"expires_in": 300,
|
||||
"interval": 5,
|
||||
"user_code": "TEST",
|
||||
"verification_uri": "https://login.tado.com/oauth2/device",
|
||||
"verification_uri_complete": "https://login.tado.com/oauth2/device?user_code=TEST"
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
"""Test the Tado config flow."""
|
||||
|
||||
from http import HTTPStatus
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import MagicMock, patch
|
||||
import threading
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import PyTado
|
||||
from PyTado.http import DeviceActivationStatus
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.tado.config_flow import NoHomes
|
||||
from homeassistant.components.tado.config_flow import TadoException
|
||||
from homeassistant.components.tado.const import (
|
||||
CONF_FALLBACK,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -26,92 +26,186 @@ from homeassistant.helpers.service_info.zeroconf import (
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _get_mock_tado_api(get_me=None) -> MagicMock:
|
||||
mock_tado = MagicMock()
|
||||
if isinstance(get_me, Exception):
|
||||
type(mock_tado).get_me = MagicMock(side_effect=get_me)
|
||||
else:
|
||||
type(mock_tado).get_me = MagicMock(return_value=get_me)
|
||||
return mock_tado
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the full flow of the config flow."""
|
||||
|
||||
event = threading.Event()
|
||||
|
||||
def mock_tado_api_device_activation() -> None:
|
||||
# Simulate the device activation process
|
||||
event.wait(timeout=5)
|
||||
|
||||
mock_tado_api.device_activation = mock_tado_api_device_activation
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
event.set()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "home name"
|
||||
assert result["data"] == {CONF_REFRESH_TOKEN: "refresh"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_full_flow_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the full flow of the config when reauthticating."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="ABC-123-DEF-456",
|
||||
data={CONF_REFRESH_TOKEN: "totally_refresh_for_reauth"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
# The no user input
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
event = threading.Event()
|
||||
|
||||
def mock_tado_api_device_activation() -> None:
|
||||
# Simulate the device activation process
|
||||
event.wait(timeout=5)
|
||||
|
||||
mock_tado_api.device_activation = mock_tado_api_device_activation
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
event.set()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "home name"
|
||||
assert result["data"] == {CONF_REFRESH_TOKEN: "refresh"}
|
||||
|
||||
|
||||
async def test_auth_timeout(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the auth timeout."""
|
||||
mock_tado_api.device_activation_status.return_value = DeviceActivationStatus.PENDING
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "timeout"
|
||||
|
||||
mock_tado_api.device_activation_status.return_value = (
|
||||
DeviceActivationStatus.COMPLETED
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "timeout"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "home name"
|
||||
assert result["data"] == {CONF_REFRESH_TOKEN: "refresh"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_no_homes(hass: HomeAssistant, mock_tado_api: MagicMock) -> None:
|
||||
"""Test the full flow of the config flow."""
|
||||
mock_tado_api.get_me.return_value["homes"] = []
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == "finish_login"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_homes"
|
||||
|
||||
|
||||
async def test_tado_creation(hass: HomeAssistant) -> None:
|
||||
"""Test we handle Form Exceptions."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
side_effect=TadoException("Test exception"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(KeyError, "invalid_auth"),
|
||||
(RuntimeError, "cannot_connect"),
|
||||
(ValueError, "unknown"),
|
||||
(Exception, "timeout"),
|
||||
(TadoException, "timeout"),
|
||||
],
|
||||
)
|
||||
async def test_form_exceptions(
|
||||
hass: HomeAssistant, exception: Exception, error: str
|
||||
async def test_wait_for_login_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test we handle Form Exceptions."""
|
||||
"""Test that an exception in wait for login is handled properly."""
|
||||
mock_tado_api.device_activation.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
side_effect=exception,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
# Test a retry to recover, upon failure
|
||||
mock_tado_api = _get_mock_tado_api(get_me={"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_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "myhome"
|
||||
assert result["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
# @joostlek: I think the timeout step is not rightfully named, but heck, it works
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE
|
||||
assert result["step_id"] == error
|
||||
|
||||
|
||||
async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_tado_api: MagicMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test config flow options."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={"username": "test-username"})
|
||||
entry.add_to_hass(hass)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.async_setup_entry",
|
||||
return_value=True,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
entry.entry_id, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
@ -119,125 +213,17 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
result["flow_id"],
|
||||
{CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_FALLBACK: CONST_OVERLAY_TADO_DEFAULT}
|
||||
|
||||
|
||||
async def test_create_entry(hass: HomeAssistant) -> None:
|
||||
"""Test we can setup though the user path."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_tado_api = _get_mock_tado_api(get_me={"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_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "myhome"
|
||||
assert result["data"] == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
response_mock = MagicMock()
|
||||
type(response_mock).status_code = HTTPStatus.UNAUTHORIZED
|
||||
mock_tado_api = _get_mock_tado_api(
|
||||
get_me=requests.HTTPError(response=response_mock)
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
return_value=mock_tado_api,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
response_mock = MagicMock()
|
||||
type(response_mock).status_code = HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
mock_tado_api = _get_mock_tado_api(
|
||||
get_me=requests.HTTPError(response=response_mock)
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
return_value=mock_tado_api,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_no_homes(hass: HomeAssistant) -> None:
|
||||
"""Test we handle no homes error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_tado_api = _get_mock_tado_api(get_me={"homes": []})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
return_value=mock_tado_api,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"username": "test-username", "password": "test-password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_homes"}
|
||||
|
||||
|
||||
async def test_form_homekit(hass: HomeAssistant) -> None:
|
||||
async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None:
|
||||
"""Test that we abort from homekit if tado is already setup."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_HOMEKIT},
|
||||
context={"source": SOURCE_HOMEKIT},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
@ -248,8 +234,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None:
|
||||
type="mock_type",
|
||||
),
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE
|
||||
flow = next(
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
@ -264,7 +249,7 @@ async def test_form_homekit(hass: HomeAssistant) -> None:
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_HOMEKIT},
|
||||
context={"source": SOURCE_HOMEKIT},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
@ -276,77 +261,3 @@ async def test_form_homekit(hass: HomeAssistant) -> None:
|
||||
),
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(PyTado.exceptions.TadoWrongCredentialsException, "invalid_auth"),
|
||||
(RuntimeError, "cannot_connect"),
|
||||
(NoHomes, "no_homes"),
|
||||
(ValueError, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_flow(
|
||||
hass: HomeAssistant, exception: Exception, error: str
|
||||
) -> None:
|
||||
"""Test re-configuration flow."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
},
|
||||
unique_id="unique_id",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tado.config_flow.Tado",
|
||||
side_effect=exception,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_tado_api = _get_mock_tado_api(get_me={"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,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
entry = hass.config_entries.async_get_entry(entry.entry_id)
|
||||
assert entry
|
||||
assert entry.title == "Mock Title"
|
||||
assert entry.data == {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
"home_id": 1,
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
|
||||
from PyTado.interface import Tado
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tado import TadoDataUpdateCoordinator
|
||||
from homeassistant.components.tado import CONF_REFRESH_TOKEN, TadoDataUpdateCoordinator
|
||||
from homeassistant.components.tado.const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
@ -28,13 +28,13 @@ def entry(request: pytest.FixtureRequest) -> MockConfigEntry:
|
||||
request.param if hasattr(request, "param") else CONST_OVERLAY_TADO_DEFAULT
|
||||
)
|
||||
return MockConfigEntry(
|
||||
version=1,
|
||||
minor_version=1,
|
||||
version=2,
|
||||
domain=DOMAIN,
|
||||
title="Tado",
|
||||
data={
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_REFRESH_TOKEN: "test-refresh",
|
||||
},
|
||||
options={
|
||||
"fallback": fallback,
|
||||
|
30
tests/components/tado/test_init.py
Normal file
30
tests/components/tado/test_init.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Test the Tado integration."""
|
||||
|
||||
from homeassistant.components.tado import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_v1_migration(hass: HomeAssistant) -> None:
|
||||
"""Test migration from v1 to v2 config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_USERNAME: "test",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
unique_id="1",
|
||||
version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.version == 2
|
||||
assert CONF_USERNAME not in entry.data
|
||||
assert CONF_PASSWORD not in entry.data
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
@ -2,8 +2,7 @@
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.tado import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.components.tado import CONF_REFRESH_TOKEN, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
@ -178,9 +177,16 @@ async def async_init_integration(
|
||||
"https://my.tado.com/api/v2/homes/1/zones/1/state",
|
||||
text=load_fixture(zone_1_state_fixture),
|
||||
)
|
||||
m.post(
|
||||
"https://login.tado.com/oauth2/token",
|
||||
text=load_fixture(token_fixture),
|
||||
)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"},
|
||||
version=2,
|
||||
data={
|
||||
CONF_REFRESH_TOKEN: "mock-token",
|
||||
},
|
||||
options={"fallback": "NEXT_TIME_BLOCK"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
Loading…
x
Reference in New Issue
Block a user