diff --git a/CODEOWNERS b/CODEOWNERS index 1c0efd92609..b3ae7e94215 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1209,8 +1209,8 @@ build.json @home-assistant/supervisor /homeassistant/components/synology_srm/ @aerialls /homeassistant/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001 -/homeassistant/components/tado/ @michaelarnauts -/tests/components/tado/ @michaelarnauts +/homeassistant/components/tado/ @michaelarnauts @chiefdragon +/tests/components/tado/ @michaelarnauts @chiefdragon /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 691ca639656..1cd21634c8e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -25,6 +25,7 @@ from .const import ( DATA, DOMAIN, INSIDE_TEMPERATURE_MEASUREMENT, + PRESET_AUTO, SIGNAL_TADO_UPDATE_RECEIVED, TEMP_OFFSET, UPDATE_LISTENER, @@ -151,6 +152,7 @@ class TadoConnector: self.data = { "device": {}, "weather": {}, + "geofence": {}, "zone": {}, } @@ -175,11 +177,7 @@ class TadoConnector: """Update the registered zones.""" self.update_devices() self.update_zones() - self.data["weather"] = self.tado.getWeather() - dispatcher_send( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "weather", "data"), - ) + self.update_home() def update_devices(self): """Update the device data from Tado.""" @@ -250,10 +248,29 @@ class TadoConnector: SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone_id), ) + def update_home(self): + """Update the home data from Tado.""" + try: + self.data["weather"] = self.tado.getWeather() + self.data["geofence"] = self.tado.getHomeState() + dispatcher_send( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), + ) + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating weather and geofence data" + ) + return + def get_capabilities(self, zone_id): """Return the capabilities of the devices.""" return self.tado.getCapabilities(zone_id) + def get_auto_geofencing_supported(self): + """Return whether the Tado Home supports auto geofencing.""" + return self.tado.getAutoGeofencingSupported() + def reset_zone_overlay(self, zone_id): """Reset the zone back to the default operation.""" self.tado.resetZoneOverlay(zone_id) @@ -263,12 +280,17 @@ class TadoConnector: self, presence=PRESET_HOME, ): - """Set the presence to home or away.""" + """Set the presence to home, away or auto.""" if presence == PRESET_AWAY: self.tado.setAway() elif presence == PRESET_HOME: self.tado.setHome() + elif presence == PRESET_AUTO: + self.tado.setAuto() + + # Update everything when changing modes self.update_zones() + self.update_home() def set_zone_overlay( self, diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index cab3c42184e..2b8bc4060d6 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -44,8 +44,10 @@ from .const import ( HA_TO_TADO_HVAC_MODE_MAP, HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, + PRESET_AUTO, SIGNAL_TADO_UPDATE_RECEIVED, - SUPPORT_PRESET, + SUPPORT_PRESET_AUTO, + SUPPORT_PRESET_MANUAL, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, TADO_SWING_OFF, @@ -245,6 +247,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._attr_name = zone_name self._attr_temperature_unit = UnitOfTemperature.CELSIUS + self._attr_translation_key = DOMAIN + self._device_info = device_info self._device_id = self._device_info["shortSerialNo"] @@ -274,21 +278,31 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._current_tado_swing_mode = TADO_SWING_OFF self._tado_zone_data = None + self._tado_geofence_data = None self._tado_zone_temp_offset = {} + self._async_update_home_data() self._async_update_zone_data() async def async_added_to_hass(self) -> None: """Register for sensor updates.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado.home_id, "home", "data"), + self._async_update_home_callback, + ) + ) + self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( self._tado.home_id, "zone", self.zone_id ), - self._async_update_callback, + self._async_update_zone_callback, ) ) @@ -346,7 +360,11 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def preset_mode(self): - """Return the current preset mode (home, away).""" + """Return the current preset mode (home, away or auto).""" + + if "presenceLocked" in self._tado_geofence_data: + if not self._tado_geofence_data["presenceLocked"]: + return PRESET_AUTO if self._tado_zone_data.is_away: return PRESET_AWAY return PRESET_HOME @@ -354,7 +372,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def preset_modes(self): """Return a list of available preset modes.""" - return SUPPORT_PRESET + if self._tado.get_auto_geofencing_supported(): + return SUPPORT_PRESET_AUTO + return SUPPORT_PRESET_MANUAL def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -501,11 +521,22 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode @callback - def _async_update_callback(self): + def _async_update_zone_callback(self): """Load tado data and update state.""" self._async_update_zone_data() self.async_write_ha_state() + @callback + def _async_update_home_data(self): + """Load tado geofencing data into zone.""" + self._tado_geofence_data = self._tado.data["geofence"] + + @callback + def _async_update_home_callback(self): + """Load tado data and update state.""" + self._async_update_home_data() + self.async_write_ha_state() + def _normalize_target_temp_for_hvac_mode(self): # Set a target temperature if we don't have any # This can happen when we switch from Off to On diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 94d074c4066..9366a18b6fe 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -153,8 +153,14 @@ TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP. DEFAULT_TADO_PRECISION = 0.1 -SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] +# Constant for Auto Geolocation mode +PRESET_AUTO = "auto" +SUPPORT_PRESET_AUTO = [PRESET_AWAY, PRESET_HOME, PRESET_AUTO] +SUPPORT_PRESET_MANUAL = [PRESET_AWAY, PRESET_HOME] + +SENSOR_DATA_CATEGORY_WEATHER = "weather" +SENSOR_DATA_CATEGORY_GEOFENCE = "geofence" TADO_SWING_OFF = "OFF" TADO_SWING_ON = "ON" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 86ace76c849..62f7a377239 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -1,7 +1,7 @@ { "domain": "tado", "name": "Tado", - "codeowners": ["@michaelarnauts"], + "codeowners": ["@michaelarnauts", "@chiefdragon"], "config_flow": true, "dhcp": [ { @@ -14,5 +14,5 @@ }, "iot_class": "cloud_polling", "loggers": ["PyTado"], - "requirements": ["python-tado==0.12.0"] + "requirements": ["python-tado==0.15.0"] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index d218e9ca933..7742f6b0dca 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -23,6 +23,8 @@ from .const import ( CONDITIONS_MAP, DATA, DOMAIN, + SENSOR_DATA_CATEGORY_GEOFENCE, + SENSOR_DATA_CATEGORY_WEATHER, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_AIR_CONDITIONING, TYPE_HEATING, @@ -47,6 +49,7 @@ class TadoSensorEntityDescription( """Describes Tado sensor entity.""" attributes_fn: Callable[[Any], dict[Any, StateType]] | None = None + data_category: str | None = None HOME_SENSORS = [ @@ -60,6 +63,7 @@ HOME_SENSORS = [ native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + data_category=SENSOR_DATA_CATEGORY_WEATHER, ), TadoSensorEntityDescription( key="solar percentage", @@ -70,12 +74,35 @@ HOME_SENSORS = [ }, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + data_category=SENSOR_DATA_CATEGORY_WEATHER, ), TadoSensorEntityDescription( key="weather condition", name="Weather condition", state_fn=lambda data: format_condition(data["weatherState"]["value"]), attributes_fn=lambda data: {"time": data["weatherState"]["timestamp"]}, + data_category=SENSOR_DATA_CATEGORY_WEATHER, + ), + TadoSensorEntityDescription( + key="tado mode", + name="Tado mode", + # pylint: disable=unnecessary-lambda + state_fn=lambda data: get_tado_mode(data), + data_category=SENSOR_DATA_CATEGORY_GEOFENCE, + ), + TadoSensorEntityDescription( + key="geofencing mode", + name="Geofencing mode", + # pylint: disable=unnecessary-lambda + state_fn=lambda data: get_geofencing_mode(data), + data_category=SENSOR_DATA_CATEGORY_GEOFENCE, + ), + TadoSensorEntityDescription( + key="automatic geofencing", + name="Automatic geofencing", + # pylint: disable=unnecessary-lambda + state_fn=lambda data: get_automatic_geofencing(data), + data_category=SENSOR_DATA_CATEGORY_GEOFENCE, ), ] @@ -145,6 +172,39 @@ def format_condition(condition: str) -> str: return condition +def get_tado_mode(data) -> str | None: + """Return Tado Mode based on Presence attribute.""" + if "presence" in data: + return data["presence"] + return None + + +def get_automatic_geofencing(data) -> bool: + """Return whether Automatic Geofencing is enabled based on Presence Locked attribute.""" + if "presenceLocked" in data: + if data["presenceLocked"]: + return False + return True + return False + + +def get_geofencing_mode(data) -> str: + """Return Geofencing Mode based on Presence and Presence Locked attributes.""" + tado_mode = "" + tado_mode = data.get("presence", "unknown") + + geofencing_switch_mode = "" + if "presenceLocked" in data: + if data["presenceLocked"]: + geofencing_switch_mode = "manual" + else: + geofencing_switch_mode = "auto" + else: + geofencing_switch_mode = "manual" + + return f"{tado_mode.capitalize()} ({geofencing_switch_mode.capitalize()})" + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -200,9 +260,7 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.home_id, "weather", "data" - ), + SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado.home_id, "home", "data"), self._async_update_callback, ) ) @@ -219,13 +277,19 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): """Handle update callbacks.""" try: tado_weather_data = self._tado.data["weather"] + tado_geofence_data = self._tado.data["geofence"] except KeyError: return - self._attr_native_value = self.entity_description.state_fn(tado_weather_data) + if self.entity_description.data_category is not None: + if self.entity_description.data_category == SENSOR_DATA_CATEGORY_WEATHER: + tado_sensor_data = tado_weather_data + else: + tado_sensor_data = tado_geofence_data + self._attr_native_value = self.entity_description.state_fn(tado_sensor_data) if self.entity_description.attributes_fn is not None: self._attr_extra_state_attributes = self.entity_description.attributes_fn( - tado_weather_data + tado_sensor_data ) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index e1bf1a1406d..3decfe3cd0c 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -29,5 +29,18 @@ "title": "Adjust Tado options." } } + }, + "entity": { + "climate": { + "tado": { + "state_attributes": { + "preset_mode": { + "state": { + "auto": "Auto" + } + } + } + } + } } } diff --git a/requirements_all.txt b/requirements_all.txt index 1c7e3e96618..ef7c225d8c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2123,7 +2123,7 @@ python-smarttub==0.0.33 python-songpal==0.15.2 # homeassistant.components.tado -python-tado==0.12.0 +python-tado==0.15.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf4f88351a3..689c731a057 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ python-smarttub==0.0.33 python-songpal==0.15.2 # homeassistant.components.tado -python-tado==0.12.0 +python-tado==0.15.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/tests/components/tado/fixtures/home_state.json b/tests/components/tado/fixtures/home_state.json new file mode 100644 index 00000000000..dc073fbfd78 --- /dev/null +++ b/tests/components/tado/fixtures/home_state.json @@ -0,0 +1,4 @@ +{ + "presence": "HOME", + "presenceLocked": false +} diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 35e017278aa..fd4ae87ac64 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -22,8 +22,8 @@ async def test_air_con(hass: HomeAssistant) -> None: "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], "max_temp": 31.0, "min_temp": 16.0, - "preset_mode": "home", - "preset_modes": ["away", "home"], + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], "supported_features": 25, "target_temp_step": 1, "temperature": 17.8, @@ -49,8 +49,8 @@ async def test_heater(hass: HomeAssistant) -> None: "hvac_modes": ["off", "auto", "heat"], "max_temp": 31.0, "min_temp": 16.0, - "preset_mode": "home", - "preset_modes": ["away", "home"], + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], "supported_features": 17, "target_temp_step": 1, "temperature": 20.5, @@ -78,8 +78,8 @@ async def test_smartac_with_swing(hass: HomeAssistant) -> None: "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], "max_temp": 30.0, "min_temp": 16.0, - "preset_mode": "home", - "preset_modes": ["away", "home"], + "preset_mode": "auto", + "preset_modes": ["away", "home", "auto"], "swing_modes": ["on", "off"], "supported_features": 57, "target_temp_step": 1.0, diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 899d2ce1f21..21e0e255ed1 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -19,6 +19,7 @@ async def async_init_integration( devices_fixture = "tado/devices.json" me_fixture = "tado/me.json" weather_fixture = "tado/weather.json" + home_state_fixture = "tado/home_state.json" zones_fixture = "tado/zones.json" zone_states_fixture = "tado/zone_states.json" @@ -61,6 +62,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/weather", text=load_fixture(weather_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/state", + text=load_fixture(home_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/devices", text=load_fixture(devices_fixture),