mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
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
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."""
|
||||
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):
|
||||
|
@ -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."""
|
||||
|
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
|
||||
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"
|
||||
|
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",
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
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,
|
||||
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."""
|
||||
|
@ -113,6 +113,7 @@ FLOWS = [
|
||||
"spotify",
|
||||
"starline",
|
||||
"synology_dsm",
|
||||
"tado",
|
||||
"tellduslive",
|
||||
"tesla",
|
||||
"toon",
|
||||
|
@ -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"
|
||||
}
|
||||
|
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.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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user