diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 4087183bfe5..4b0203acda3 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -31,6 +31,7 @@ PLATFORMS = [ Platform.CLIMATE, Platform.DEVICE_TRACKER, Platform.SENSOR, + Platform.SWITCH, Platform.WATER_HEATER, ] diff --git a/homeassistant/components/tado/coordinator.py b/homeassistant/components/tado/coordinator.py index 6e932c8ccfc..559bc4a16fb 100644 --- a/homeassistant/components/tado/coordinator.py +++ b/homeassistant/components/tado/coordinator.py @@ -342,6 +342,17 @@ class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): except RequestException as err: raise UpdateFailed(f"Error setting Tado meter reading: {err}") from err + async def set_child_lock(self, device_id: str, enabled: bool) -> None: + """Set child lock of device.""" + try: + await self.hass.async_add_executor_job( + self._tado.set_child_lock, + device_id, + enabled, + ) + except RequestException as exc: + raise HomeAssistantError(f"Error setting Tado child lock: {exc}") from exc + class TadoMobileDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]): """Class to manage the mobile devices from Tado via PyTado.""" diff --git a/homeassistant/components/tado/icons.json b/homeassistant/components/tado/icons.json index c799bef0260..65b86359950 100644 --- a/homeassistant/components/tado/icons.json +++ b/homeassistant/components/tado/icons.json @@ -1,4 +1,14 @@ { + "entity": { + "switch": { + "child_lock": { + "default": "mdi:lock-open-variant", + "state": { + "on": "mdi:lock" + } + } + } + }, "services": { "set_climate_timer": { "service": "mdi:timer" diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index f1550517457..ff1afc3c03d 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -64,6 +64,11 @@ } } }, + "switch": { + "child_lock": { + "name": "Child lock" + } + }, "sensor": { "outdoor_temperature": { "name": "Outdoor temperature" diff --git a/homeassistant/components/tado/switch.py b/homeassistant/components/tado/switch.py new file mode 100644 index 00000000000..b3f355462b8 --- /dev/null +++ b/homeassistant/components/tado/switch.py @@ -0,0 +1,88 @@ +"""Module for Tado child lock switch entity.""" + +import logging +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import TadoConfigEntry +from .entity import TadoDataUpdateCoordinator, TadoZoneEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: TadoConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Tado switch platform.""" + + tado = entry.runtime_data.coordinator + entities: list[TadoChildLockSwitchEntity] = [] + for zone in tado.zones: + zoneChildLockSupported = ( + len(zone["devices"]) > 0 and "childLockEnabled" in zone["devices"][0] + ) + + if not zoneChildLockSupported: + continue + + entities.append( + TadoChildLockSwitchEntity( + tado, zone["name"], zone["id"], zone["devices"][0] + ) + ) + async_add_entities(entities, True) + + +class TadoChildLockSwitchEntity(TadoZoneEntity, SwitchEntity): + """Representation of a Tado child lock switch entity.""" + + _attr_translation_key = "child_lock" + + def __init__( + self, + coordinator: TadoDataUpdateCoordinator, + zone_name: str, + zone_id: int, + device_info: dict[str, Any], + ) -> None: + """Initialize the Tado child lock switch entity.""" + super().__init__(zone_name, coordinator.home_id, zone_id, coordinator) + + self._device_info = device_info + self._device_id = self._device_info["shortSerialNo"] + self._attr_unique_id = f"{zone_id} {coordinator.home_id} child-lock" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.coordinator.set_child_lock(self._device_id, True) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.set_child_lock(self._device_id, False) + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_callback() + super()._handle_coordinator_update() + + @callback + def _async_update_callback(self) -> None: + """Handle update callbacks.""" + try: + self._device_info = self.coordinator.data["device"][self._device_id] + except KeyError: + _LOGGER.error( + "Could not update child lock info for device %s in zone %s", + self._device_id, + self.zone_name, + ) + else: + self._attr_is_on = self._device_info.get("childLockEnabled", False) is True diff --git a/tests/components/tado/fixtures/devices.json b/tests/components/tado/fixtures/devices.json index 6d990082b96..a9313ae051b 100644 --- a/tests/components/tado/fixtures/devices.json +++ b/tests/components/tado/fixtures/devices.json @@ -15,5 +15,24 @@ "value": true }, "shortSerialNo": "WR1" + }, + { + "duties": ["ZONE_UI", "ZONE_DRIVER", "ZONE_LEADER"], + "currentFwVersion": "59.4", + "deviceType": "WR02", + "serialNo": "WR4", + "shortSerialNo": "WR4", + "commandTableUploadState": "FINISHED", + "connectionState": { + "value": true, + "timestamp": "2020-03-23T18:30:07.377Z" + }, + "accessPointWiFi": { + "ssid": "tado8480" + }, + "characteristics": { + "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] + }, + "childLockEnabled": false } ] diff --git a/tests/components/tado/fixtures/zones.json b/tests/components/tado/fixtures/zones.json index e1d2ec759ba..acc4612b393 100644 --- a/tests/components/tado/fixtures/zones.json +++ b/tests/components/tado/fixtures/zones.json @@ -27,7 +27,8 @@ }, "characteristics": { "capabilities": ["INSIDE_TEMPERATURE_MEASUREMENT", "IDENTIFY"] - } + }, + "childLockEnabled": false } ], "dateCreated": "2019-11-28T15:58:48.968Z", diff --git a/tests/components/tado/test_switch.py b/tests/components/tado/test_switch.py new file mode 100644 index 00000000000..2112f3a1ac7 --- /dev/null +++ b/tests/components/tado/test_switch.py @@ -0,0 +1,47 @@ +"""The sensor tests for the tado platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + +CHILD_LOCK_SWITCH_ENTITY = "switch.baseboard_heater_child_lock" + + +async def test_child_lock(hass: HomeAssistant) -> None: + """Test creation of child lock entity.""" + + await async_init_integration(hass) + state = hass.states.get(CHILD_LOCK_SWITCH_ENTITY) + assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + ("method", "expected"), [(SERVICE_TURN_ON, True), (SERVICE_TURN_OFF, False)] +) +async def test_set_child_lock(hass: HomeAssistant, method, expected) -> None: + """Test enable child lock on switch.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.tado.PyTado.interface.api.Tado.set_child_lock" + ) as mock_set_state: + await hass.services.async_call( + SWITCH_DOMAIN, + method, + {ATTR_ENTITY_ID: CHILD_LOCK_SWITCH_ENTITY}, + blocking=True, + ) + + mock_set_state.assert_called_once() + assert mock_set_state.call_args[0][1] is expected