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:
parent
761943e1e6
commit
9672db0354
12 changed files with 174 additions and 29 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -29,5 +29,18 @@
|
|||
"title": "Adjust Tado options."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"tado": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"auto": "Auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
4
tests/components/tado/fixtures/home_state.json
Normal file
4
tests/components/tado/fixtures/home_state.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presence": "HOME",
|
||||
"presenceLocked": false
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Reference in a new issue