Add new preset to Tado to enable geofencing mode (#92877)

* Add new preset to Tado to enable geofencing mode
Add new 'auto' preset mode to enable Tado to be set to auto geofencing
mode.  The existing ‘home’ and ‘away’ presets switched Tado into manual
geofencing mode and there was no way to restore it to auto mode.
Note 1: Since preset modes (home, away and auto) apply to the Tado home
holistically, irrespective of the Tado climate entity used to select
the preset, three new sensors have been added to display the state of
the Tado home
Note 2: Auto mode is only supported if the Auto Assist skill is enabled
in the owner's Tado home. Various checks have been added to ensure the
Tado supports auto geofencing and if it is not supported, the preset is
not listed in the preset modes available

* Update codeowners in manifest.json

* Update main codeowners file for Tado component
This commit is contained in:
chiefdragon 2023-05-23 18:08:00 +01:00 committed by GitHub
parent 761943e1e6
commit 9672db0354
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 174 additions and 29 deletions

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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"

View file

@ -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"]
}

View file

@ -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
)

View file

@ -29,5 +29,18 @@
"title": "Adjust Tado options."
}
}
},
"entity": {
"climate": {
"tado": {
"state_attributes": {
"preset_mode": {
"state": {
"auto": "Auto"
}
}
}
}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
{
"presence": "HOME",
"presenceLocked": false
}

View file

@ -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,

View file

@ -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),