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:
J. Nick Koston 2020-04-12 13:42:36 -05:00 committed by GitHub
parent f8f8dddca7
commit 4b1626a748
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 682 additions and 138 deletions

View 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."
}
}
}
}

View File

@ -1,25 +1,34 @@
"""Support for the (unofficial) Tado API."""
import asyncio
from datetime import timedelta
import logging
from PyTado.interface import Tado
from requests import RequestException
import requests.exceptions
import voluptuous as vol
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.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
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.event import async_track_time_interval
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__)
DOMAIN = "tado"
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}"
TADO_COMPONENTS = ["sensor", "climate", "water_heater"]
@ -43,45 +52,106 @@ CONFIG_SCHEMA = vol.Schema(
)
def setup(hass, config):
"""Set up of the Tado component."""
acc_list = config[DOMAIN]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Tado component."""
api_data_list = []
hass.data.setdefault(DOMAIN, {})
for acc in acc_list:
username = acc[CONF_USERNAME]
password = acc[CONF_PASSWORD]
fallback = acc[CONF_FALLBACK]
if DOMAIN not in config:
return True
tadoconnector = TadoConnector(hass, username, password, fallback)
if not tadoconnector.setup():
continue
# 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,
for conf in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf,
)
)
return True
async def async_setup_entry(hass: 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:
"""An object to store the Tado data."""
@ -108,19 +178,12 @@ class TadoConnector:
def setup(self):
"""Connect to Tado and fetch the zones."""
try:
self.tado = Tado(self._username, self._password)
except (RuntimeError, RequestException) as exc:
_LOGGER.error("Unable to connect: %s", exc)
return False
self.tado = Tado(self._username, self._password)
self.tado.setDebugging(True)
# Load zones and devices
self.zones = self.tado.getZones()
self.devices = self.tado.getMe()["homes"]
self.device_id = self.devices[0]["id"]
return True
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):

View File

@ -14,11 +14,11 @@ from homeassistant.components.climate.const import (
SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
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 . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import (
CONST_FAN_AUTO,
CONST_FAN_OFF,
@ -30,9 +30,11 @@ from .const import (
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_MODE,
DATA,
DOMAIN,
HA_TO_TADO_FAN_MODE_MAP,
HA_TO_TADO_HVAC_MODE_MAP,
ORDERED_KNOWN_TADO_MODES,
SIGNAL_TADO_UPDATE_RECEIVED,
SUPPORT_PRESET,
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
TADO_MODES_WITH_NO_TEMP_SETTING,
@ -42,30 +44,37 @@ from .const import (
TYPE_AIR_CONDITIONING,
TYPE_HEATING,
)
from .entity import TadoZoneEntity
_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."""
if discovery_info is None:
return
api_list = hass.data[DOMAIN][DATA]
entities = []
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)
tado = hass.data[DOMAIN][entry.entry_id][DATA]
entities = await hass.async_add_executor_job(_generate_entities, tado)
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."""
capabilities = tado.get_capabilities(zone_id)
_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_fan_modes,
support_flags,
zone,
)
return entity
class TadoClimate(ClimateDevice):
class TadoClimate(TadoZoneEntity, ClimateDevice):
"""Representation of a Tado climate entity."""
def __init__(
@ -170,11 +180,12 @@ class TadoClimate(ClimateDevice):
supported_hvac_modes,
supported_fan_modes,
support_flags,
device_info,
):
"""Initialize of Tado climate entity."""
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_type = zone_type
self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
@ -206,6 +217,7 @@ class TadoClimate(ClimateDevice):
self._undo_dispatcher = None
self._tado_zone_data = None
self._async_update_zone_data()
async def async_will_remove_from_hass(self):
@ -237,11 +249,6 @@ class TadoClimate(ClimateDevice):
"""Return the unique id."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Do not poll."""
return False
@property
def current_humidity(self):
"""Return the current humidity."""

View 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."""

View File

@ -46,6 +46,7 @@ TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
# Configuration
CONF_FALLBACK = "fallback"
DATA = "data"
UPDATE_TRACK = "update_track"
# Types
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
@ -135,3 +136,14 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
TADO_SWING_OFF = "OFF"
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"

View 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

View File

@ -3,5 +3,9 @@
"name": "Tado",
"documentation": "https://www.home-assistant.io/integrations/tado",
"requirements": ["python-tado==0.6.0"],
"codeowners": ["@michaelarnauts", "@bdraco"]
"codeowners": ["@michaelarnauts", "@bdraco"],
"config_flow": true,
"homekit": {
"models": ["tado", "AC02"]
}
}

View File

@ -1,13 +1,23 @@
"""Support for Tado sensors for each zone."""
import logging
from homeassistant.config_entries import ConfigEntry
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.entity import Entity
from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER
from .const import (
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__)
@ -39,50 +49,53 @@ ZONE_SENSORS = {
DEVICE_SENSORS = ["tado bridge status"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the sensor platform."""
api_list = hass.data[DOMAIN][DATA]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities
):
"""Set up the Tado sensor platform."""
tado = hass.data[DOMAIN][entry.entry_id][DATA]
# Create zone sensors
zones = tado.zones
devices = tado.devices
entities = []
for tado in api_list:
# Create zone sensors
zones = tado.zones
devices = tado.devices
for zone in zones:
zone_type = zone["type"]
if zone_type not in ZONE_SENSORS:
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue
for zone in zones:
zone_type = zone["type"]
if zone_type not in ZONE_SENSORS:
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue
entities.extend(
[
TadoZoneSensor(
tado, zone["name"], zone["id"], variable, zone["devices"][0]
)
for variable in ZONE_SENSORS[zone_type]
]
)
entities.extend(
[
TadoZoneSensor(tado, zone["name"], zone["id"], variable)
for variable in ZONE_SENSORS[zone_type]
]
)
# Create device sensors
for device in devices:
entities.extend(
[
TadoDeviceSensor(tado, device["name"], device["id"], variable, device)
for variable in DEVICE_SENSORS
]
)
# Create device sensors
for device in devices:
entities.extend(
[
TadoDeviceSensor(tado, device["name"], device["id"], variable)
for variable in DEVICE_SENSORS
]
)
add_entities(entities, True)
if entities:
async_add_entities(entities, True)
class TadoZoneSensor(Entity):
class TadoZoneSensor(TadoZoneEntity, Entity):
"""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."""
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_variable = zone_variable
@ -148,11 +161,6 @@ class TadoZoneSensor(Entity):
if self.zone_variable == "humidity":
return "mdi:water-percent"
@property
def should_poll(self):
"""Do not poll."""
return False
@callback
def _async_update_callback(self):
"""Update and write state."""
@ -223,10 +231,11 @@ class TadoZoneSensor(Entity):
class TadoDeviceSensor(Entity):
"""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."""
self._tado = tado
self._device_info = device_info
self.device_name = device_name
self.device_id = device_id
self.device_variable = device_variable
@ -289,3 +298,13 @@ class TadoDeviceSensor(Entity):
if self.device_variable == "tado bridge status":
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,
}

View 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."
}
}
}
}

View File

@ -6,11 +6,11 @@ from homeassistant.components.water_heater import (
SUPPORT_TARGET_TEMPERATURE,
WaterHeaterDevice,
)
from homeassistant.config_entries import ConfigEntry
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 . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import (
CONST_HVAC_HEAT,
CONST_MODE_AUTO,
@ -21,8 +21,11 @@ from .const import (
CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TIMER,
DATA,
DOMAIN,
SIGNAL_TADO_UPDATE_RECEIVED,
TYPE_HOT_WATER,
)
from .entity import TadoZoneEntity
_LOGGER = logging.getLogger(__name__)
@ -44,25 +47,31 @@ WATER_HEATER_MAP_TADO = {
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."""
if discovery_info is None:
return
api_list = hass.data[DOMAIN][DATA]
entities = []
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)
tado = hass.data[DOMAIN][entry.entry_id][DATA]
entities = await hass.async_add_executor_job(_generate_entities, tado)
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."""
capabilities = tado.get_capabilities(zone_id)
@ -77,13 +86,19 @@ def create_water_heater_entity(tado, name: str, zone_id: int):
max_temp = None
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
class TadoWaterHeater(WaterHeaterDevice):
class TadoWaterHeater(TadoZoneEntity, WaterHeaterDevice):
"""Representation of a Tado water heater."""
def __init__(
@ -94,11 +109,13 @@ class TadoWaterHeater(WaterHeaterDevice):
supports_temperature_control,
min_temp,
max_temp,
device_info,
):
"""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._unique_id = f"{zone_id} {tado.device_id}"
@ -149,11 +166,6 @@ class TadoWaterHeater(WaterHeaterDevice):
"""Return the unique id."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Do not poll."""
return False
@property
def current_operation(self):
"""Return current readable operation mode."""

View File

@ -113,6 +113,7 @@ FLOWS = [
"spotify",
"starline",
"synology_dsm",
"tado",
"tellduslive",
"tesla",
"toon",

View File

@ -47,6 +47,7 @@ ZEROCONF = {
HOMEKIT = {
"819LMB": "myq",
"AC02": "tado",
"BSB002": "hue",
"Healty Home Coach": "netatmo",
"LIFX": "lifx",
@ -55,5 +56,6 @@ HOMEKIT = {
"Rachio": "rachio",
"TRADFRI": "tradfri",
"Welcome": "netatmo",
"Wemo": "wemo"
"Wemo": "wemo",
"tado": "tado"
}

View 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"

View File

@ -5,9 +5,8 @@ import requests_mock
from homeassistant.components.tado import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
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(
@ -93,8 +92,11 @@ async def async_init_integration(
"https://my.tado.com/api/v2/homes/1/zones/1/state",
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:
assert await async_setup_component(
hass, DOMAIN, {DOMAIN: {CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}}
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()