From 29e0ef604e5effdd917bfcf851f56d0d71559a3a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 17 Nov 2021 15:08:37 +0100 Subject: [PATCH] Add typing to deCONZ Climate and Cover platforms (#59610) --- homeassistant/components/deconz/climate.py | 50 +++++++++++++++------- homeassistant/components/deconz/cover.py | 50 ++++++++++++++-------- tests/components/deconz/test_climate.py | 4 +- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 4641bd26d43..85ab4b17a1e 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,6 +1,9 @@ """Support for deCONZ climate devices.""" from __future__ import annotations +from collections.abc import ValuesView +from typing import Any + from pydeconz.sensor import ( THERMOSTAT_FAN_MODE_AUTO, THERMOSTAT_FAN_MODE_HIGH, @@ -42,13 +45,15 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" @@ -89,7 +94,11 @@ PRESET_MODE_TO_DECONZ = { DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the deCONZ climate devices. Thermostats are based on the same device class as sensors in deCONZ. @@ -98,9 +107,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_climate(sensors=gateway.api.sensors.values()): + def async_add_climate( + sensors: list[Thermostat] + | ValuesView[Thermostat] = gateway.api.sensors.values(), + ) -> None: """Add climate devices from deCONZ.""" - entities = [] + entities: list[DeconzThermostat] = [] for sensor in sensors: @@ -131,9 +143,11 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" TYPE = DOMAIN + _device: Thermostat + _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, device, gateway): + def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None: """Set up thermostat device.""" super().__init__(device, gateway) @@ -167,7 +181,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): ) @property - def fan_modes(self) -> list: + def fan_modes(self) -> list[str]: """Return the list of available fan operation modes.""" return list(FAN_MODE_TO_DECONZ) @@ -181,7 +195,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): # HVAC control @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -192,7 +206,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): ) @property - def hvac_modes(self) -> list: + def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" return list(self._hvac_mode_to_deconz) @@ -231,16 +245,20 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def current_temperature(self) -> float: """Return the current temperature.""" - return self._device.temperature + return self._device.temperature # type: ignore[no-any-return] @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return the target temperature.""" - if self._device.mode == THERMOSTAT_MODE_COOL: - return self._device.cooling_setpoint - return self._device.heating_setpoint + if self._device.mode == THERMOSTAT_MODE_COOL and self._device.cooling_setpoint: + return self._device.cooling_setpoint # type: ignore[no-any-return] - async def async_set_temperature(self, **kwargs): + if self._device.heating_setpoint: + return self._device.heating_setpoint # type: ignore[no-any-return] + + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_TEMPERATURE not in kwargs: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") @@ -252,7 +270,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): await self._device.set_config(**data) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool | int]: """Return the state attributes of the thermostat.""" attr = {} diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 5cf90c4dca1..4c40e89b9f3 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,5 +1,10 @@ """Support for deCONZ covers.""" +from __future__ import annotations + +from collections.abc import ValuesView +from typing import Any + from pydeconz.light import Cover from homeassistant.components.cover import ( @@ -18,11 +23,13 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import DeconzGateway, get_gateway_from_config_entry DEVICE_CLASS = { "Level controllable output": DEVICE_CLASS_DAMPER, @@ -31,13 +38,19 @@ DEVICE_CLASS = { } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up covers for deCONZ component.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_cover(lights=gateway.api.lights.values()): + def async_add_cover( + lights: list[Cover] | ValuesView[Cover] = gateway.api.lights.values(), + ) -> None: """Add cover from deCONZ.""" entities = [] @@ -66,8 +79,9 @@ class DeconzCover(DeconzDevice, CoverEntity): """Representation of a deCONZ cover.""" TYPE = DOMAIN + _device: Cover - def __init__(self, device, gateway): + def __init__(self, device: Cover, gateway: DeconzGateway) -> None: """Set up cover device.""" super().__init__(device, gateway) @@ -85,52 +99,52 @@ class DeconzCover(DeconzDevice, CoverEntity): self._attr_device_class = DEVICE_CLASS.get(self._device.type) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of the cover.""" - return 100 - self._device.lift + return 100 - self._device.lift # type: ignore[no-any-return] @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return not self._device.is_open - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: int) -> None: """Move the cover to a specific position.""" position = 100 - kwargs[ATTR_POSITION] await self._device.set_position(lift=position) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" await self._device.open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self._device.close() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" await self._device.stop() @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current tilt position of the cover.""" if self._device.tilt is not None: - return 100 - self._device.tilt + return 100 - self._device.tilt # type: ignore[no-any-return] return None - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: int) -> None: """Tilt the cover to a specific position.""" position = 100 - kwargs[ATTR_TILT_POSITION] await self._device.set_position(tilt=position) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" await self._device.set_position(tilt=0) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" await self._device.set_position(tilt=100) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" await self._device.stop() diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 92ca38fdf4d..3febbae510b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -347,7 +347,7 @@ async def test_climate_device_with_cooling_support( "0": { "config": { "battery": 25, - "coolsetpoint": None, + "coolsetpoint": 1111, "fanmode": None, "heatsetpoint": 2222, "mode": "heat", @@ -398,8 +398,10 @@ async def test_climate_device_with_cooling_support( } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.get("climate.zen_01").state == HVAC_MODE_COOL + assert hass.states.get("climate.zen_01").attributes["temperature"] == 11.1 # Verify service calls