Config flow for tado (#33677)
* Config flow for tado * Add homekit models * self review fixes * reduce since the loop is gone * Update homeassistant/components/tado/water_heater.py Co-Authored-By: Michaël Arnauts <michael.arnauts@gmail.com> * Change identifier * Ensure fallback mode is on by default * unique ids much be str Co-authored-by: Michaël Arnauts <michael.arnauts@gmail.com>
This commit is contained in:
parent
f8f8dddca7
commit
4b1626a748
14 changed files with 682 additions and 138 deletions
35
homeassistant/components/tado/.translations/en.json
Normal file
35
homeassistant/components/tado/.translations/en.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"config" : {
|
||||||
|
"abort" : {
|
||||||
|
"already_configured" : "Device is already configured"
|
||||||
|
},
|
||||||
|
"step" : {
|
||||||
|
"user" : {
|
||||||
|
"data" : {
|
||||||
|
"password" : "Password",
|
||||||
|
"username" : "Username"
|
||||||
|
},
|
||||||
|
"title" : "Connect to your Tado account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error" : {
|
||||||
|
"unknown" : "Unexpected error",
|
||||||
|
"no_homes" : "There are no homes linked to this tado account.",
|
||||||
|
"invalid_auth" : "Invalid authentication",
|
||||||
|
"cannot_connect" : "Failed to connect, please try again"
|
||||||
|
},
|
||||||
|
"title" : "Tado"
|
||||||
|
},
|
||||||
|
"options" : {
|
||||||
|
"title" : "Tado",
|
||||||
|
"step" : {
|
||||||
|
"init" : {
|
||||||
|
"description" : "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.",
|
||||||
|
"data" : {
|
||||||
|
"fallback" : "Enable fallback mode."
|
||||||
|
},
|
||||||
|
"title" : "Adjust Tado options."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,25 +1,34 @@
|
||||||
"""Support for the (unofficial) Tado API."""
|
"""Support for the (unofficial) Tado API."""
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyTado.interface import Tado
|
from PyTado.interface import Tado
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
import requests.exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME
|
from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.discovery import load_platform
|
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from .const import CONF_FALLBACK, DATA
|
from .const import (
|
||||||
|
CONF_FALLBACK,
|
||||||
|
DATA,
|
||||||
|
DOMAIN,
|
||||||
|
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||||
|
UPDATE_LISTENER,
|
||||||
|
UPDATE_TRACK,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "tado"
|
|
||||||
|
|
||||||
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}"
|
|
||||||
|
|
||||||
TADO_COMPONENTS = ["sensor", "climate", "water_heater"]
|
TADO_COMPONENTS = ["sensor", "climate", "water_heater"]
|
||||||
|
|
||||||
|
@ -43,45 +52,106 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up of the Tado component."""
|
"""Set up the Tado component."""
|
||||||
acc_list = config[DOMAIN]
|
|
||||||
|
|
||||||
api_data_list = []
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
for acc in acc_list:
|
if DOMAIN not in config:
|
||||||
username = acc[CONF_USERNAME]
|
return True
|
||||||
password = acc[CONF_PASSWORD]
|
|
||||||
fallback = acc[CONF_FALLBACK]
|
|
||||||
|
|
||||||
tadoconnector = TadoConnector(hass, username, password, fallback)
|
for conf in config[DOMAIN]:
|
||||||
if not tadoconnector.setup():
|
hass.async_create_task(
|
||||||
continue
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
|
||||||
# Do first update
|
)
|
||||||
tadoconnector.update()
|
|
||||||
|
|
||||||
api_data_list.append(tadoconnector)
|
|
||||||
# Poll for updates in the background
|
|
||||||
hass.helpers.event.track_time_interval(
|
|
||||||
# we're using here tadoconnector as a parameter of lambda
|
|
||||||
# to capture actual value instead of closuring of latest value
|
|
||||||
lambda now, tc=tadoconnector: tc.update(),
|
|
||||||
SCAN_INTERVAL,
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
|
||||||
hass.data[DOMAIN][DATA] = api_data_list
|
|
||||||
|
|
||||||
# Load components
|
|
||||||
for component in TADO_COMPONENTS:
|
|
||||||
load_platform(
|
|
||||||
hass, component, DOMAIN, {}, config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Tado from a config entry."""
|
||||||
|
|
||||||
|
_async_import_options_from_data_if_missing(hass, entry)
|
||||||
|
|
||||||
|
username = entry.data[CONF_USERNAME]
|
||||||
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
fallback = entry.options.get(CONF_FALLBACK, True)
|
||||||
|
|
||||||
|
tadoconnector = TadoConnector(hass, username, password, fallback)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await hass.async_add_executor_job(tadoconnector.setup)
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("Failed to login to tado")
|
||||||
|
return False
|
||||||
|
except RuntimeError as exc:
|
||||||
|
_LOGGER.error("Failed to setup tado: %s", exc)
|
||||||
|
return ConfigEntryNotReady
|
||||||
|
except requests.exceptions.HTTPError as ex:
|
||||||
|
if ex.response.status_code > 400 and ex.response.status_code < 500:
|
||||||
|
_LOGGER.error("Failed to login to tado: %s", ex)
|
||||||
|
return False
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
# Do first update
|
||||||
|
await hass.async_add_executor_job(tadoconnector.update)
|
||||||
|
|
||||||
|
# Poll for updates in the background
|
||||||
|
update_track = async_track_time_interval(
|
||||||
|
hass, lambda now: tadoconnector.update(), SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_listener = entry.add_update_listener(_async_update_listener)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
DATA: tadoconnector,
|
||||||
|
UPDATE_TRACK: update_track,
|
||||||
|
UPDATE_LISTENER: update_listener,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in TADO_COMPONENTS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
options = dict(entry.options)
|
||||||
|
if CONF_FALLBACK not in options:
|
||||||
|
options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, True)
|
||||||
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in TADO_COMPONENTS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry.entry_id][UPDATE_TRACK]()
|
||||||
|
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]()
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
class TadoConnector:
|
class TadoConnector:
|
||||||
"""An object to store the Tado data."""
|
"""An object to store the Tado data."""
|
||||||
|
|
||||||
|
@ -108,19 +178,12 @@ class TadoConnector:
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
"""Connect to Tado and fetch the zones."""
|
"""Connect to Tado and fetch the zones."""
|
||||||
try:
|
self.tado = Tado(self._username, self._password)
|
||||||
self.tado = Tado(self._username, self._password)
|
|
||||||
except (RuntimeError, RequestException) as exc:
|
|
||||||
_LOGGER.error("Unable to connect: %s", exc)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.tado.setDebugging(True)
|
self.tado.setDebugging(True)
|
||||||
|
|
||||||
# Load zones and devices
|
# Load zones and devices
|
||||||
self.zones = self.tado.getZones()
|
self.zones = self.tado.getZones()
|
||||||
self.devices = self.tado.getMe()["homes"]
|
self.devices = self.tado.getMe()["homes"]
|
||||||
self.device_id = self.devices[0]["id"]
|
self.device_id = self.devices[0]["id"]
|
||||||
return True
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
|
|
|
@ -14,11 +14,11 @@ from homeassistant.components.climate.const import (
|
||||||
SUPPORT_SWING_MODE,
|
SUPPORT_SWING_MODE,
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS
|
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONST_FAN_AUTO,
|
CONST_FAN_AUTO,
|
||||||
CONST_FAN_OFF,
|
CONST_FAN_OFF,
|
||||||
|
@ -30,9 +30,11 @@ from .const import (
|
||||||
CONST_OVERLAY_MANUAL,
|
CONST_OVERLAY_MANUAL,
|
||||||
CONST_OVERLAY_TADO_MODE,
|
CONST_OVERLAY_TADO_MODE,
|
||||||
DATA,
|
DATA,
|
||||||
|
DOMAIN,
|
||||||
HA_TO_TADO_FAN_MODE_MAP,
|
HA_TO_TADO_FAN_MODE_MAP,
|
||||||
HA_TO_TADO_HVAC_MODE_MAP,
|
HA_TO_TADO_HVAC_MODE_MAP,
|
||||||
ORDERED_KNOWN_TADO_MODES,
|
ORDERED_KNOWN_TADO_MODES,
|
||||||
|
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||||
SUPPORT_PRESET,
|
SUPPORT_PRESET,
|
||||||
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
|
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
|
||||||
TADO_MODES_WITH_NO_TEMP_SETTING,
|
TADO_MODES_WITH_NO_TEMP_SETTING,
|
||||||
|
@ -42,30 +44,37 @@ from .const import (
|
||||||
TYPE_AIR_CONDITIONING,
|
TYPE_AIR_CONDITIONING,
|
||||||
TYPE_HEATING,
|
TYPE_HEATING,
|
||||||
)
|
)
|
||||||
|
from .entity import TadoZoneEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||||
|
):
|
||||||
"""Set up the Tado climate platform."""
|
"""Set up the Tado climate platform."""
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
api_list = hass.data[DOMAIN][DATA]
|
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||||
entities = []
|
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||||
|
|
||||||
for tado in api_list:
|
|
||||||
for zone in tado.zones:
|
|
||||||
if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]:
|
|
||||||
entity = create_climate_entity(tado, zone["name"], zone["id"])
|
|
||||||
if entity:
|
|
||||||
entities.append(entity)
|
|
||||||
|
|
||||||
if entities:
|
if entities:
|
||||||
add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
def create_climate_entity(tado, name: str, zone_id: int):
|
def _generate_entities(tado):
|
||||||
|
"""Create all climate entities."""
|
||||||
|
entities = []
|
||||||
|
for zone in tado.zones:
|
||||||
|
if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]:
|
||||||
|
entity = create_climate_entity(
|
||||||
|
tado, zone["name"], zone["id"], zone["devices"][0]
|
||||||
|
)
|
||||||
|
if entity:
|
||||||
|
entities.append(entity)
|
||||||
|
return entities
|
||||||
|
|
||||||
|
|
||||||
|
def create_climate_entity(tado, name: str, zone_id: int, zone: dict):
|
||||||
"""Create a Tado climate entity."""
|
"""Create a Tado climate entity."""
|
||||||
capabilities = tado.get_capabilities(zone_id)
|
capabilities = tado.get_capabilities(zone_id)
|
||||||
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
|
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
|
||||||
|
@ -148,11 +157,12 @@ def create_climate_entity(tado, name: str, zone_id: int):
|
||||||
supported_hvac_modes,
|
supported_hvac_modes,
|
||||||
supported_fan_modes,
|
supported_fan_modes,
|
||||||
support_flags,
|
support_flags,
|
||||||
|
zone,
|
||||||
)
|
)
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
|
|
||||||
class TadoClimate(ClimateDevice):
|
class TadoClimate(TadoZoneEntity, ClimateDevice):
|
||||||
"""Representation of a Tado climate entity."""
|
"""Representation of a Tado climate entity."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -170,11 +180,12 @@ class TadoClimate(ClimateDevice):
|
||||||
supported_hvac_modes,
|
supported_hvac_modes,
|
||||||
supported_fan_modes,
|
supported_fan_modes,
|
||||||
support_flags,
|
support_flags,
|
||||||
|
device_info,
|
||||||
):
|
):
|
||||||
"""Initialize of Tado climate entity."""
|
"""Initialize of Tado climate entity."""
|
||||||
self._tado = tado
|
self._tado = tado
|
||||||
|
super().__init__(zone_name, device_info, tado.device_id, zone_id)
|
||||||
|
|
||||||
self.zone_name = zone_name
|
|
||||||
self.zone_id = zone_id
|
self.zone_id = zone_id
|
||||||
self.zone_type = zone_type
|
self.zone_type = zone_type
|
||||||
self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
|
self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
|
||||||
|
@ -206,6 +217,7 @@ class TadoClimate(ClimateDevice):
|
||||||
|
|
||||||
self._undo_dispatcher = None
|
self._undo_dispatcher = None
|
||||||
self._tado_zone_data = None
|
self._tado_zone_data = None
|
||||||
|
|
||||||
self._async_update_zone_data()
|
self._async_update_zone_data()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
|
@ -237,11 +249,6 @@ class TadoClimate(ClimateDevice):
|
||||||
"""Return the unique id."""
|
"""Return the unique id."""
|
||||||
return self._unique_id
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self) -> bool:
|
|
||||||
"""Do not poll."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_humidity(self):
|
def current_humidity(self):
|
||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
|
|
148
homeassistant/components/tado/config_flow.py
Normal file
148
homeassistant/components/tado/config_flow.py
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
"""Config flow for Tado integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PyTado.interface import Tado
|
||||||
|
import requests.exceptions
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import CONF_FALLBACK, UNIQUE_ID
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema(
|
||||||
|
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""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.getMe)
|
||||||
|
except KeyError:
|
||||||
|
raise InvalidAuth
|
||||||
|
except RuntimeError:
|
||||||
|
raise CannotConnect
|
||||||
|
except requests.exceptions.HTTPError as ex:
|
||||||
|
if ex.response.status_code > 400 and ex.response.status_code < 500:
|
||||||
|
raise InvalidAuth
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
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 ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Tado."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
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: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_homekit(self, homekit_info):
|
||||||
|
"""Handle HomeKit discovery."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
# We can see tado on the network to tell them to configure
|
||||||
|
# it, but since the device will not give up the account it is
|
||||||
|
# bound to and there can be multiple tado devices on a single
|
||||||
|
# account, we avoid showing the device as discovered once
|
||||||
|
# they already have one configured as they can always
|
||||||
|
# add a new one via "+"
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Handle import."""
|
||||||
|
if self._username_already_configured(user_input):
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
def _username_already_configured(self, user_input):
|
||||||
|
"""See if we already have a username matching user input configured."""
|
||||||
|
existing_username = {
|
||||||
|
entry.data[CONF_USERNAME] for entry in self._async_current_entries()
|
||||||
|
}
|
||||||
|
return user_input[CONF_USERNAME] in existing_username
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for tado."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK)
|
||||||
|
): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoHomes(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate the account has no homes."""
|
|
@ -46,6 +46,7 @@ TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
|
||||||
# Configuration
|
# Configuration
|
||||||
CONF_FALLBACK = "fallback"
|
CONF_FALLBACK = "fallback"
|
||||||
DATA = "data"
|
DATA = "data"
|
||||||
|
UPDATE_TRACK = "update_track"
|
||||||
|
|
||||||
# Types
|
# Types
|
||||||
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
|
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
|
||||||
|
@ -135,3 +136,14 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
|
||||||
|
|
||||||
TADO_SWING_OFF = "OFF"
|
TADO_SWING_OFF = "OFF"
|
||||||
TADO_SWING_ON = "ON"
|
TADO_SWING_ON = "ON"
|
||||||
|
|
||||||
|
DOMAIN = "tado"
|
||||||
|
|
||||||
|
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}"
|
||||||
|
UNIQUE_ID = "unique_id"
|
||||||
|
|
||||||
|
DEFAULT_NAME = "Tado"
|
||||||
|
|
||||||
|
TADO_BRIDGE = "Tado Bridge"
|
||||||
|
|
||||||
|
UPDATE_LISTENER = "update_listener"
|
||||||
|
|
37
homeassistant/components/tado/entity.py
Normal file
37
homeassistant/components/tado/entity.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
"""Base class for August entity."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DEFAULT_NAME, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TadoZoneEntity(Entity):
|
||||||
|
"""Base implementation for tado device."""
|
||||||
|
|
||||||
|
def __init__(self, zone_name, device_info, device_id, zone_id):
|
||||||
|
"""Initialize an August device."""
|
||||||
|
super().__init__()
|
||||||
|
self._device_zone_id = f"{device_id}_{zone_id}"
|
||||||
|
self._device_info = device_info
|
||||||
|
self.zone_name = zone_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device_info of the device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._device_zone_id)},
|
||||||
|
"name": self.zone_name,
|
||||||
|
"manufacturer": DEFAULT_NAME,
|
||||||
|
"sw_version": self._device_info["currentFwVersion"],
|
||||||
|
"model": self._device_info["deviceType"],
|
||||||
|
"via_device": (DOMAIN, self._device_info["serialNo"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Do not poll."""
|
||||||
|
return False
|
|
@ -3,5 +3,9 @@
|
||||||
"name": "Tado",
|
"name": "Tado",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tado",
|
"documentation": "https://www.home-assistant.io/integrations/tado",
|
||||||
"requirements": ["python-tado==0.6.0"],
|
"requirements": ["python-tado==0.6.0"],
|
||||||
"codeowners": ["@michaelarnauts", "@bdraco"]
|
"codeowners": ["@michaelarnauts", "@bdraco"],
|
||||||
|
"config_flow": true,
|
||||||
|
"homekit": {
|
||||||
|
"models": ["tado", "AC02"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
"""Support for Tado sensors for each zone."""
|
"""Support for Tado sensors for each zone."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE
|
from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
|
from .const import (
|
||||||
from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER
|
DATA,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DOMAIN,
|
||||||
|
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||||
|
TADO_BRIDGE,
|
||||||
|
TYPE_AIR_CONDITIONING,
|
||||||
|
TYPE_HEATING,
|
||||||
|
TYPE_HOT_WATER,
|
||||||
|
)
|
||||||
|
from .entity import TadoZoneEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -39,50 +49,53 @@ ZONE_SENSORS = {
|
||||||
DEVICE_SENSORS = ["tado bridge status"]
|
DEVICE_SENSORS = ["tado bridge status"]
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
"""Set up the sensor platform."""
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||||
api_list = hass.data[DOMAIN][DATA]
|
):
|
||||||
|
"""Set up the Tado sensor platform."""
|
||||||
|
|
||||||
|
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||||
|
# Create zone sensors
|
||||||
|
zones = tado.zones
|
||||||
|
devices = tado.devices
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
for tado in api_list:
|
for zone in zones:
|
||||||
# Create zone sensors
|
zone_type = zone["type"]
|
||||||
zones = tado.zones
|
if zone_type not in ZONE_SENSORS:
|
||||||
devices = tado.devices
|
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
|
||||||
|
continue
|
||||||
|
|
||||||
for zone in zones:
|
entities.extend(
|
||||||
zone_type = zone["type"]
|
[
|
||||||
if zone_type not in ZONE_SENSORS:
|
TadoZoneSensor(
|
||||||
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
|
tado, zone["name"], zone["id"], variable, zone["devices"][0]
|
||||||
continue
|
)
|
||||||
|
for variable in ZONE_SENSORS[zone_type]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
entities.extend(
|
# Create device sensors
|
||||||
[
|
for device in devices:
|
||||||
TadoZoneSensor(tado, zone["name"], zone["id"], variable)
|
entities.extend(
|
||||||
for variable in ZONE_SENSORS[zone_type]
|
[
|
||||||
]
|
TadoDeviceSensor(tado, device["name"], device["id"], variable, device)
|
||||||
)
|
for variable in DEVICE_SENSORS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Create device sensors
|
if entities:
|
||||||
for device in devices:
|
async_add_entities(entities, True)
|
||||||
entities.extend(
|
|
||||||
[
|
|
||||||
TadoDeviceSensor(tado, device["name"], device["id"], variable)
|
|
||||||
for variable in DEVICE_SENSORS
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
add_entities(entities, True)
|
|
||||||
|
|
||||||
|
|
||||||
class TadoZoneSensor(Entity):
|
class TadoZoneSensor(TadoZoneEntity, Entity):
|
||||||
"""Representation of a tado Sensor."""
|
"""Representation of a tado Sensor."""
|
||||||
|
|
||||||
def __init__(self, tado, zone_name, zone_id, zone_variable):
|
def __init__(self, tado, zone_name, zone_id, zone_variable, device_info):
|
||||||
"""Initialize of the Tado Sensor."""
|
"""Initialize of the Tado Sensor."""
|
||||||
self._tado = tado
|
self._tado = tado
|
||||||
|
super().__init__(zone_name, device_info, tado.device_id, zone_id)
|
||||||
|
|
||||||
self.zone_name = zone_name
|
|
||||||
self.zone_id = zone_id
|
self.zone_id = zone_id
|
||||||
self.zone_variable = zone_variable
|
self.zone_variable = zone_variable
|
||||||
|
|
||||||
|
@ -148,11 +161,6 @@ class TadoZoneSensor(Entity):
|
||||||
if self.zone_variable == "humidity":
|
if self.zone_variable == "humidity":
|
||||||
return "mdi:water-percent"
|
return "mdi:water-percent"
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Do not poll."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_callback(self):
|
def _async_update_callback(self):
|
||||||
"""Update and write state."""
|
"""Update and write state."""
|
||||||
|
@ -223,10 +231,11 @@ class TadoZoneSensor(Entity):
|
||||||
class TadoDeviceSensor(Entity):
|
class TadoDeviceSensor(Entity):
|
||||||
"""Representation of a tado Sensor."""
|
"""Representation of a tado Sensor."""
|
||||||
|
|
||||||
def __init__(self, tado, device_name, device_id, device_variable):
|
def __init__(self, tado, device_name, device_id, device_variable, device_info):
|
||||||
"""Initialize of the Tado Sensor."""
|
"""Initialize of the Tado Sensor."""
|
||||||
self._tado = tado
|
self._tado = tado
|
||||||
|
|
||||||
|
self._device_info = device_info
|
||||||
self.device_name = device_name
|
self.device_name = device_name
|
||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
self.device_variable = device_variable
|
self.device_variable = device_variable
|
||||||
|
@ -289,3 +298,13 @@ class TadoDeviceSensor(Entity):
|
||||||
|
|
||||||
if self.device_variable == "tado bridge status":
|
if self.device_variable == "tado bridge status":
|
||||||
self._state = data.get("connectionState", {}).get("value", False)
|
self._state = data.get("connectionState", {}).get("value", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return the device_info of the device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.device_id)},
|
||||||
|
"name": self.device_name,
|
||||||
|
"manufacturer": DEFAULT_NAME,
|
||||||
|
"model": TADO_BRIDGE,
|
||||||
|
}
|
||||||
|
|
35
homeassistant/components/tado/strings.json
Normal file
35
homeassistant/components/tado/strings.json
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Password",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"title": "Connect to your Tado account"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"unknown": "Unexpected error",
|
||||||
|
"no_homes": "There are no homes linked to this tado account.",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"cannot_connect": "Failed to connect, please try again"
|
||||||
|
},
|
||||||
|
"title": "Tado"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"title": "Tado",
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"description": "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.",
|
||||||
|
"data": {
|
||||||
|
"fallback": "Enable fallback mode."
|
||||||
|
},
|
||||||
|
"title": "Adjust Tado options."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,11 @@ from homeassistant.components.water_heater import (
|
||||||
SUPPORT_TARGET_TEMPERATURE,
|
SUPPORT_TARGET_TEMPERATURE,
|
||||||
WaterHeaterDevice,
|
WaterHeaterDevice,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONST_HVAC_HEAT,
|
CONST_HVAC_HEAT,
|
||||||
CONST_MODE_AUTO,
|
CONST_MODE_AUTO,
|
||||||
|
@ -21,8 +21,11 @@ from .const import (
|
||||||
CONST_OVERLAY_TADO_MODE,
|
CONST_OVERLAY_TADO_MODE,
|
||||||
CONST_OVERLAY_TIMER,
|
CONST_OVERLAY_TIMER,
|
||||||
DATA,
|
DATA,
|
||||||
|
DOMAIN,
|
||||||
|
SIGNAL_TADO_UPDATE_RECEIVED,
|
||||||
TYPE_HOT_WATER,
|
TYPE_HOT_WATER,
|
||||||
)
|
)
|
||||||
|
from .entity import TadoZoneEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -44,25 +47,31 @@ WATER_HEATER_MAP_TADO = {
|
||||||
SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE
|
SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
|
||||||
|
):
|
||||||
"""Set up the Tado water heater platform."""
|
"""Set up the Tado water heater platform."""
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
api_list = hass.data[DOMAIN][DATA]
|
tado = hass.data[DOMAIN][entry.entry_id][DATA]
|
||||||
entities = []
|
entities = await hass.async_add_executor_job(_generate_entities, tado)
|
||||||
|
|
||||||
for tado in api_list:
|
|
||||||
for zone in tado.zones:
|
|
||||||
if zone["type"] == TYPE_HOT_WATER:
|
|
||||||
entity = create_water_heater_entity(tado, zone["name"], zone["id"])
|
|
||||||
entities.append(entity)
|
|
||||||
|
|
||||||
if entities:
|
if entities:
|
||||||
add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
|
||||||
def create_water_heater_entity(tado, name: str, zone_id: int):
|
def _generate_entities(tado):
|
||||||
|
"""Create all water heater entities."""
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for zone in tado.zones:
|
||||||
|
if zone["type"] == TYPE_HOT_WATER:
|
||||||
|
entity = create_water_heater_entity(tado, zone["name"], zone["id"], zone)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
return entities
|
||||||
|
|
||||||
|
|
||||||
|
def create_water_heater_entity(tado, name: str, zone_id: int, zone: str):
|
||||||
"""Create a Tado water heater device."""
|
"""Create a Tado water heater device."""
|
||||||
capabilities = tado.get_capabilities(zone_id)
|
capabilities = tado.get_capabilities(zone_id)
|
||||||
|
|
||||||
|
@ -77,13 +86,19 @@ def create_water_heater_entity(tado, name: str, zone_id: int):
|
||||||
max_temp = None
|
max_temp = None
|
||||||
|
|
||||||
entity = TadoWaterHeater(
|
entity = TadoWaterHeater(
|
||||||
tado, name, zone_id, supports_temperature_control, min_temp, max_temp
|
tado,
|
||||||
|
name,
|
||||||
|
zone_id,
|
||||||
|
supports_temperature_control,
|
||||||
|
min_temp,
|
||||||
|
max_temp,
|
||||||
|
zone["devices"][0],
|
||||||
)
|
)
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
|
|
||||||
class TadoWaterHeater(WaterHeaterDevice):
|
class TadoWaterHeater(TadoZoneEntity, WaterHeaterDevice):
|
||||||
"""Representation of a Tado water heater."""
|
"""Representation of a Tado water heater."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -94,11 +109,13 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||||
supports_temperature_control,
|
supports_temperature_control,
|
||||||
min_temp,
|
min_temp,
|
||||||
max_temp,
|
max_temp,
|
||||||
|
device_info,
|
||||||
):
|
):
|
||||||
"""Initialize of Tado water heater entity."""
|
"""Initialize of Tado water heater entity."""
|
||||||
self._tado = tado
|
|
||||||
|
|
||||||
self.zone_name = zone_name
|
self._tado = tado
|
||||||
|
super().__init__(zone_name, device_info, tado.device_id, zone_id)
|
||||||
|
|
||||||
self.zone_id = zone_id
|
self.zone_id = zone_id
|
||||||
self._unique_id = f"{zone_id} {tado.device_id}"
|
self._unique_id = f"{zone_id} {tado.device_id}"
|
||||||
|
|
||||||
|
@ -149,11 +166,6 @@ class TadoWaterHeater(WaterHeaterDevice):
|
||||||
"""Return the unique id."""
|
"""Return the unique id."""
|
||||||
return self._unique_id
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self) -> bool:
|
|
||||||
"""Do not poll."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self):
|
def current_operation(self):
|
||||||
"""Return current readable operation mode."""
|
"""Return current readable operation mode."""
|
||||||
|
|
|
@ -113,6 +113,7 @@ FLOWS = [
|
||||||
"spotify",
|
"spotify",
|
||||||
"starline",
|
"starline",
|
||||||
"synology_dsm",
|
"synology_dsm",
|
||||||
|
"tado",
|
||||||
"tellduslive",
|
"tellduslive",
|
||||||
"tesla",
|
"tesla",
|
||||||
"toon",
|
"toon",
|
||||||
|
|
|
@ -47,6 +47,7 @@ ZEROCONF = {
|
||||||
|
|
||||||
HOMEKIT = {
|
HOMEKIT = {
|
||||||
"819LMB": "myq",
|
"819LMB": "myq",
|
||||||
|
"AC02": "tado",
|
||||||
"BSB002": "hue",
|
"BSB002": "hue",
|
||||||
"Healty Home Coach": "netatmo",
|
"Healty Home Coach": "netatmo",
|
||||||
"LIFX": "lifx",
|
"LIFX": "lifx",
|
||||||
|
@ -55,5 +56,6 @@ HOMEKIT = {
|
||||||
"Rachio": "rachio",
|
"Rachio": "rachio",
|
||||||
"TRADFRI": "tradfri",
|
"TRADFRI": "tradfri",
|
||||||
"Welcome": "netatmo",
|
"Welcome": "netatmo",
|
||||||
"Wemo": "wemo"
|
"Wemo": "wemo",
|
||||||
|
"tado": "tado"
|
||||||
}
|
}
|
||||||
|
|
167
tests/components/tado/test_config_flow.py
Normal file
167
tests/components/tado/test_config_flow.py
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
"""Test the Tado config flow."""
|
||||||
|
from asynctest import MagicMock, patch
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant import config_entries, setup
|
||||||
|
from homeassistant.components.tado.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mock_tado_api(getMe=None):
|
||||||
|
mock_tado = MagicMock()
|
||||||
|
if isinstance(getMe, Exception):
|
||||||
|
type(mock_tado).getMe = MagicMock(side_effect=getMe)
|
||||||
|
else:
|
||||||
|
type(mock_tado).getMe = MagicMock(return_value=getMe)
|
||||||
|
return mock_tado
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass):
|
||||||
|
"""Test we can setup though the user path."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
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", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.tado.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "myhome"
|
||||||
|
assert result2["data"] == {
|
||||||
|
"username": "test-username",
|
||||||
|
"password": "test-password",
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass):
|
||||||
|
"""Test we can import."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
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", return_value=True
|
||||||
|
) as mock_setup, 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"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "myhome"
|
||||||
|
assert result["data"] == {
|
||||||
|
"username": "test-username",
|
||||||
|
"password": "test-password",
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass):
|
||||||
|
"""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 = 401
|
||||||
|
mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""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 = 500
|
||||||
|
mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_no_homes(hass):
|
||||||
|
"""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(getMe={"homes": []})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{"username": "test-username", "password": "test-password"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "no_homes"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_homekit(hass):
|
||||||
|
"""Test that we abort from homekit if tado is already setup."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "homekit"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "homekit"}
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
|
@ -5,9 +5,8 @@ import requests_mock
|
||||||
from homeassistant.components.tado import DOMAIN
|
from homeassistant.components.tado import DOMAIN
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.setup import async_setup_component
|
|
||||||
|
|
||||||
from tests.common import load_fixture
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
async def async_init_integration(
|
async def async_init_integration(
|
||||||
|
@ -93,8 +92,11 @@ async def async_init_integration(
|
||||||
"https://my.tado.com/api/v2/homes/1/zones/1/state",
|
"https://my.tado.com/api/v2/homes/1/zones/1/state",
|
||||||
text=load_fixture(zone_1_state_fixture),
|
text=load_fixture(zone_1_state_fixture),
|
||||||
)
|
)
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
if not skip_setup:
|
if not skip_setup:
|
||||||
assert await async_setup_component(
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
hass, DOMAIN, {DOMAIN: {CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue