mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
Add overlay options to Tado (#65886)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
09f6785956
commit
e76170fbfd
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user