diff --git a/CODEOWNERS b/CODEOWNERS index 8ad2f367ed2..4c04e307949 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1003,8 +1003,8 @@ build.json @home-assistant/supervisor /homeassistant/components/syslog/ @fabaff /homeassistant/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001 -/homeassistant/components/tado/ @michaelarnauts -/tests/components/tado/ @michaelarnauts +/homeassistant/components/tado/ @michaelarnauts @north3221 +/tests/components/tado/ @michaelarnauts @north3221 /homeassistant/components/tag/ @balloob @dmulcahey /tests/components/tag/ @balloob @dmulcahey /homeassistant/components/tailscale/ @frenck diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 42fc806ab54..9a5e1eb9c1e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from PyTado.interface import Tado -from PyTado.zone import TadoZone from requests import RequestException import requests.exceptions @@ -19,6 +18,7 @@ from homeassistant.util import Throttle from .const import ( CONF_FALLBACK, + CONST_OVERLAY_TADO_MODE, DATA, DOMAIN, INSIDE_TEMPERATURE_MEASUREMENT, @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] 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) @@ -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): options = dict(entry.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) @@ -213,23 +213,8 @@ class TadoConnector: _LOGGER.error("Unable to connect to Tado while updating zones") return - for zone in self.zones: - zone_id = zone["id"] - _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"]), - ) + for zone in zone_states: + self.update_zone(int(zone)) def update_zone(self, zone_id): """Update the internal data from Tado.""" diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6489c78cbe3..3e6dd2cb130 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CONST_EXCLUSIVE_OVERLAY_GROUP, CONST_FAN_AUTO, CONST_FAN_OFF, CONST_MODE_AUTO, @@ -32,10 +33,14 @@ from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TADO_OPTIONS, CONST_OVERLAY_TIMER, DATA, DOMAIN, + HA_TERMINATION_DURATION, + HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, HA_TO_TADO_HVAC_MODE_MAP, ORDERED_KNOWN_TADO_MODES, @@ -58,12 +63,16 @@ _LOGGER = logging.getLogger(__name__) SERVICE_CLIMATE_TIMER = "set_climate_timer" ATTR_TIME_PERIOD = "time_period" +ATTR_REQUESTED_OVERLAY = "requested_overlay" 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() ), - 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" @@ -379,11 +388,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): # the device is switching states 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.""" 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): @@ -464,7 +476,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def extra_state_attributes(self): """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): """Set swing modes for the device.""" @@ -474,6 +493,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def _async_update_zone_data(self): """Load tado data into zone.""" self._tado_zone_data = self._tado.data["zone"][self.zone_id] + # Assign offset values to mapped attributes for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): if ( @@ -518,6 +538,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): fan_mode=None, swing_mode=None, duration=None, + overlay_mode=None, ): """Send new target temperature to Tado.""" @@ -559,22 +580,41 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.reset_zone_overlay(self.zone_id) 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( - "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.zone_name, self.zone_id, self._target_temp, 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 if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING: # A temperature cannot be passed with these modes diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index e2a67c21b5d..95b415c5acc 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -11,12 +11,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback 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__) 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( { - vol.Required( + vol.Optional( 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) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 2c86fa2d642..291d02cb403 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -95,6 +95,17 @@ CONST_OVERLAY_TADO_MODE = ( ) 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_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 @@ -180,3 +191,7 @@ TADO_TO_HA_OFFSET_MAP = { TADO_OFFSET_CELSIUS: HA_OFFSET_CELSIUS, TADO_OFFSET_FAHRENHEIT: HA_OFFSET_FAHRENHEIT, } + +# Constants for Overlay Default settings +HA_TERMINATION_TYPE = "default_overlay_type" +HA_TERMINATION_DURATION = "default_overlay_seconds" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 529b4bcfb97..078c821eeb4 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": ["python-tado==0.12.0"], - "codeowners": ["@michaelarnauts"], + "codeowners": ["@michaelarnauts", "@north3221"], "config_flow": true, "homekit": { "models": ["tado", "AC02"] diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index d3aaa71cbbc..97d4891551d 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -6,14 +6,6 @@ set_climate_timer: integration: tado domain: climate 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: name: Temperature description: Temperature to set climate entity to @@ -24,6 +16,26 @@ set_climate_timer: max: 100 step: 0.5 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: name: Set water heater timer diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index e0eb90f7ddc..e1bf1a1406d 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -22,9 +22,9 @@ "options": { "step": { "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": { - "fallback": "Enable fallback mode." + "fallback": "Choose fallback mode." }, "title": "Adjust Tado options." } diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index a5e23b6021a..899d2ce1f21 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -146,7 +146,9 @@ async def async_init_integration( text=load_fixture(zone_1_state_fixture), ) 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)