Add overlay options to Tado (#65886)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
north3221 2022-03-30 06:59:03 +01:00 committed by GitHub
parent 09f6785956
commit e76170fbfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 108 additions and 51 deletions

View File

@ -1003,8 +1003,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/syslog/ @fabaff /homeassistant/components/syslog/ @fabaff
/homeassistant/components/system_bridge/ @timmo001 /homeassistant/components/system_bridge/ @timmo001
/tests/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001
/homeassistant/components/tado/ @michaelarnauts /homeassistant/components/tado/ @michaelarnauts @north3221
/tests/components/tado/ @michaelarnauts /tests/components/tado/ @michaelarnauts @north3221
/homeassistant/components/tag/ @balloob @dmulcahey /homeassistant/components/tag/ @balloob @dmulcahey
/tests/components/tag/ @balloob @dmulcahey /tests/components/tag/ @balloob @dmulcahey
/homeassistant/components/tailscale/ @frenck /homeassistant/components/tailscale/ @frenck

View File

@ -3,7 +3,6 @@ from datetime import timedelta
import logging import logging
from PyTado.interface import Tado from PyTado.interface import Tado
from PyTado.zone import TadoZone
from requests import RequestException from requests import RequestException
import requests.exceptions import requests.exceptions
@ -19,6 +18,7 @@ from homeassistant.util import Throttle
from .const import ( from .const import (
CONF_FALLBACK, CONF_FALLBACK,
CONST_OVERLAY_TADO_MODE,
DATA, DATA,
DOMAIN, DOMAIN,
INSIDE_TEMPERATURE_MEASUREMENT, INSIDE_TEMPERATURE_MEASUREMENT,
@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
fallback = entry.options.get(CONF_FALLBACK, True) fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
tadoconnector = TadoConnector(hass, username, password, fallback) tadoconnector = TadoConnector(hass, username, password, fallback)
@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
options = dict(entry.options) options = dict(entry.options)
if CONF_FALLBACK not in options: if CONF_FALLBACK not in options:
options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, True) options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
hass.config_entries.async_update_entry(entry, options=options) hass.config_entries.async_update_entry(entry, options=options)
@ -213,23 +213,8 @@ class TadoConnector:
_LOGGER.error("Unable to connect to Tado while updating zones") _LOGGER.error("Unable to connect to Tado while updating zones")
return return
for zone in self.zones: for zone in zone_states:
zone_id = zone["id"] self.update_zone(int(zone))
_LOGGER.debug("Updating zone %s", zone_id)
zone_state = TadoZone(zone_states[str(zone_id)], zone_id)
self.data["zone"][zone_id] = zone_state
_LOGGER.debug(
"Dispatching update to %s zone %s: %s",
self.home_id,
zone_id,
zone_state,
)
dispatcher_send(
self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone["id"]),
)
def update_zone(self, zone_id): def update_zone(self, zone_id):
"""Update the internal data from Tado.""" """Update the internal data from Tado."""

View File

@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
CONST_EXCLUSIVE_OVERLAY_GROUP,
CONST_FAN_AUTO, CONST_FAN_AUTO,
CONST_FAN_OFF, CONST_FAN_OFF,
CONST_MODE_AUTO, CONST_MODE_AUTO,
@ -32,10 +33,14 @@ from .const import (
CONST_MODE_OFF, CONST_MODE_OFF,
CONST_MODE_SMART_SCHEDULE, CONST_MODE_SMART_SCHEDULE,
CONST_OVERLAY_MANUAL, CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TADO_OPTIONS,
CONST_OVERLAY_TIMER, CONST_OVERLAY_TIMER,
DATA, DATA,
DOMAIN, DOMAIN,
HA_TERMINATION_DURATION,
HA_TERMINATION_TYPE,
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,
@ -58,12 +63,16 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_CLIMATE_TIMER = "set_climate_timer" SERVICE_CLIMATE_TIMER = "set_climate_timer"
ATTR_TIME_PERIOD = "time_period" ATTR_TIME_PERIOD = "time_period"
ATTR_REQUESTED_OVERLAY = "requested_overlay"
CLIMATE_TIMER_SCHEMA = { CLIMATE_TIMER_SCHEMA = {
vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All( vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
vol.Exclusive(ATTR_TIME_PERIOD, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.All(
cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds()
), ),
vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), vol.Exclusive(ATTR_REQUESTED_OVERLAY, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.In(
CONST_OVERLAY_TADO_OPTIONS
),
} }
SERVICE_TEMP_OFFSET = "set_climate_temperature_offset" SERVICE_TEMP_OFFSET = "set_climate_temperature_offset"
@ -379,11 +388,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
# the device is switching states # the device is switching states
return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp
def set_timer(self, time_period, temperature=None): def set_timer(self, temperature=None, time_period=None, requested_overlay=None):
"""Set the timer on the entity, and temperature if supported.""" """Set the timer on the entity, and temperature if supported."""
self._control_hvac( self._control_hvac(
hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period hvac_mode=CONST_MODE_HEAT,
target_temp=temperature,
duration=time_period,
overlay_mode=requested_overlay,
) )
def set_temp_offset(self, offset): def set_temp_offset(self, offset):
@ -464,7 +476,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return temperature offset.""" """Return temperature offset."""
return self._tado_zone_temp_offset state_attr = self._tado_zone_temp_offset
state_attr[
HA_TERMINATION_TYPE
] = self._tado_zone_data.default_overlay_termination_type
state_attr[
HA_TERMINATION_DURATION
] = self._tado_zone_data.default_overlay_termination_duration
return state_attr
def set_swing_mode(self, swing_mode): def set_swing_mode(self, swing_mode):
"""Set swing modes for the device.""" """Set swing modes for the device."""
@ -474,6 +493,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
def _async_update_zone_data(self): def _async_update_zone_data(self):
"""Load tado data into zone.""" """Load tado data into zone."""
self._tado_zone_data = self._tado.data["zone"][self.zone_id] self._tado_zone_data = self._tado.data["zone"][self.zone_id]
# Assign offset values to mapped attributes # Assign offset values to mapped attributes
for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items():
if ( if (
@ -518,6 +538,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
fan_mode=None, fan_mode=None,
swing_mode=None, swing_mode=None,
duration=None, duration=None,
overlay_mode=None,
): ):
"""Send new target temperature to Tado.""" """Send new target temperature to Tado."""
@ -559,22 +580,41 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
self._tado.reset_zone_overlay(self.zone_id) self._tado.reset_zone_overlay(self.zone_id)
return return
# If user gave duration then overlay mode needs to be timer
if duration:
overlay_mode = CONST_OVERLAY_TIMER
# If no duration or timer set to fallback setting
if overlay_mode is None:
overlay_mode = (
self._tado.fallback
if self._tado.fallback is not None
else CONST_OVERLAY_TADO_MODE
)
# If default is Tado default then look it up
if overlay_mode == CONST_OVERLAY_TADO_DEFAULT:
overlay_mode = (
self._tado_zone_data.default_overlay_termination_type
if self._tado_zone_data.default_overlay_termination_type is not None
else CONST_OVERLAY_TADO_MODE
)
# If we ended up with a timer but no duration, set a default duration
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
duration = (
self._tado_zone_data.default_overlay_termination_duration
if self._tado_zone_data.default_overlay_termination_duration is not None
else "3600"
)
_LOGGER.debug( _LOGGER.debug(
"Switching to %s for zone %s (%d) with temperature %s °C and duration %s", "Switching to %s for zone %s (%d) with temperature %s °C and duration %s using overlay %s",
self._current_tado_hvac_mode, self._current_tado_hvac_mode,
self.zone_name, self.zone_name,
self.zone_id, self.zone_id,
self._target_temp, self._target_temp,
duration, duration,
overlay_mode,
) )
overlay_mode = CONST_OVERLAY_MANUAL
if duration:
overlay_mode = CONST_OVERLAY_TIMER
elif self._tado.fallback:
# Fallback to Smart Schedule at next Schedule switch if we have fallback enabled
overlay_mode = CONST_OVERLAY_TADO_MODE
temperature_to_send = self._target_temp temperature_to_send = self._target_temp
if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING: if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING:
# A temperature cannot be passed with these modes # A temperature cannot be passed with these modes

View File

@ -11,12 +11,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from .const import CONF_FALLBACK, DOMAIN, UNIQUE_ID from .const import CONF_FALLBACK, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, UNIQUE_ID
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
) )
@ -122,9 +125,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
data_schema = vol.Schema( data_schema = vol.Schema(
{ {
vol.Required( vol.Optional(
CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK) CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK)
): bool, ): vol.In(CONST_OVERLAY_TADO_OPTIONS),
} }
) )
return self.async_show_form(step_id="init", data_schema=data_schema) return self.async_show_form(step_id="init", data_schema=data_schema)

View File

@ -95,6 +95,17 @@ CONST_OVERLAY_TADO_MODE = (
) )
CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually
CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan
CONST_OVERLAY_TADO_DEFAULT = (
"TADO_DEFAULT" # use the setting from tado zone itself (set in Tado app or webapp)
)
CONST_OVERLAY_TADO_OPTIONS = [
CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_DEFAULT,
]
CONST_EXCLUSIVE_OVERLAY_GROUP = (
"overlay_group" # Overlay group for set_climate_timer service
)
# Heat always comes first since we get the # Heat always comes first since we get the
@ -180,3 +191,7 @@ TADO_TO_HA_OFFSET_MAP = {
TADO_OFFSET_CELSIUS: HA_OFFSET_CELSIUS, TADO_OFFSET_CELSIUS: HA_OFFSET_CELSIUS,
TADO_OFFSET_FAHRENHEIT: HA_OFFSET_FAHRENHEIT, TADO_OFFSET_FAHRENHEIT: HA_OFFSET_FAHRENHEIT,
} }
# Constants for Overlay Default settings
HA_TERMINATION_TYPE = "default_overlay_type"
HA_TERMINATION_DURATION = "default_overlay_seconds"

View File

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

View File

@ -6,14 +6,6 @@ set_climate_timer:
integration: tado integration: tado
domain: climate domain: climate
fields: fields:
time_period:
name: Time period
description: Set the time period for the boost.
required: true
example: "01:30:00"
default: "01:00:00"
selector:
text:
temperature: temperature:
name: Temperature name: Temperature
description: Temperature to set climate entity to description: Temperature to set climate entity to
@ -24,6 +16,26 @@ set_climate_timer:
max: 100 max: 100
step: 0.5 step: 0.5
unit_of_measurement: '°' unit_of_measurement: '°'
time_period:
name: Time period
description: Choose this or Overlay. Set the time period for the change if you want to be specific. Alternatively use Overlay
required: false
example: "01:30:00"
default: "01:00:00"
selector:
text:
requested_overlay:
name: Overlay
description: Choose this or Time Period. Allows you to choose an overlay. MANUAL:=Overlay until user removes; NEXT_TIME_BLOCK:=Overlay until next timeblock; TADO_DEFAULT:=Overlay based on tado app setting
required: false
example: "MANUAL"
default: "TADO_DEFAULT"
selector:
select:
options:
- "NEXT_TIME_BLOCK"
- "MANUAL"
- "TADO_DEFAULT"
set_water_heater_timer: set_water_heater_timer:
name: Set water heater timer name: Set water heater timer

View File

@ -22,9 +22,9 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"description": "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.", "description": "Fallback mode lets you choose when to fallback to Smart Schedule from your manual zone overlay. (NEXT_TIME_BLOCK:= Change at next Smart Schedule change; MANUAL:= Dont change until you cancel; TADO_DEFAULT:= Change based on your setting in Tado App).",
"data": { "data": {
"fallback": "Enable fallback mode." "fallback": "Choose fallback mode."
}, },
"title": "Adjust Tado options." "title": "Adjust Tado options."
} }

View File

@ -146,7 +146,9 @@ async def async_init_integration(
text=load_fixture(zone_1_state_fixture), text=load_fixture(zone_1_state_fixture),
) )
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} domain=DOMAIN,
data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"},
options={"fallback": "NEXT_TIME_BLOCK"},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)