hass-core/homeassistant/components/deconz/climate.py
Robert Svensson 87551b7880
Improve typing of deCONZ climate platform (#69882)
* Improve typing of deCONZ climate platform

homeassistant/components/deconz/climate.py:153: error: Dict entry 0 has incompatible type "str": "bool"; expected "str": "str"  [dict-item]
homeassistant/components/deconz/climate.py:154: error: Dict entry 1 has incompatible type "str": "bool"; expected "str": "str"  [dict-item]
homeassistant/components/deconz/climate.py:176: error: Argument 1 to "get" of "Mapping" has incompatible type "Optional[Literal['off', 'low', 'medium', 'high', 'on', 'auto', 'smart']]"; expected "str"  [arg-type]
homeassistant/components/deconz/climate.py:200: error: Argument 1 to "get" of "Mapping" has incompatible type "Optional[Literal['off', 'auto', 'cool', 'heat', 'emergency heating', 'precooling', 'fan only', 'dry', 'sleep']]"; expected "str"  [arg-type]
homeassistant/components/deconz/climate.py:218: error: Argument 1 to "set_config" of "Thermostat" has incompatible type "**Dict[str, str]"; expected "Optional[int]"  [arg-type]
homeassistant/components/deconz/climate.py:218: error: Argument 1 to "set_config" of "Thermostat" has incompatible type "**Dict[str, str]"; expected "Optional[bool]"  [arg-type]
homeassistant/components/deconz/climate.py:218: error: Argument 1 to "set_config" of "Thermostat" has incompatible type "**Dict[str, str]"; expected "Optional[List[str]]"  [arg-type]
homeassistant/components/deconz/climate.py:225: error: Argument 1 to "get" of "Mapping" has incompatible type "Optional[Literal['holiday', 'auto', 'manual', 'comfort', 'eco', 'boost', 'complex']]"; expected "str"  [arg-type]
homeassistant/components/deconz/climate.py:244: error: Unused "type: ignore" comment
homeassistant/components/deconz/climate.py:250: error: Unused "type: ignore" comment
homeassistant/components/deconz/climate.py:253: error: Unused "type: ignore" comment

* Simplify populating supported_hvac_modes
Fix tests
2022-04-14 23:19:42 +03:00

280 lines
8.7 KiB
Python

"""Support for deCONZ climate devices."""
from __future__ import annotations
from typing import Any
from pydeconz.sensor import (
THERMOSTAT_FAN_MODE_AUTO,
THERMOSTAT_FAN_MODE_HIGH,
THERMOSTAT_FAN_MODE_LOW,
THERMOSTAT_FAN_MODE_MEDIUM,
THERMOSTAT_FAN_MODE_OFF,
THERMOSTAT_FAN_MODE_ON,
THERMOSTAT_FAN_MODE_SMART,
THERMOSTAT_MODE_AUTO,
THERMOSTAT_MODE_COOL,
THERMOSTAT_MODE_HEAT,
THERMOSTAT_MODE_OFF,
THERMOSTAT_PRESET_AUTO,
THERMOSTAT_PRESET_BOOST,
THERMOSTAT_PRESET_COMFORT,
THERMOSTAT_PRESET_COMPLEX,
THERMOSTAT_PRESET_ECO,
THERMOSTAT_PRESET_HOLIDAY,
THERMOSTAT_PRESET_MANUAL,
Thermostat,
)
from homeassistant.components.climate import DOMAIN, ClimateEntity, ClimateEntityFeature
from homeassistant.components.climate.const import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
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 DeconzGateway, get_gateway_from_config_entry
DECONZ_FAN_SMART = "smart"
FAN_MODE_TO_DECONZ = {
DECONZ_FAN_SMART: THERMOSTAT_FAN_MODE_SMART,
FAN_AUTO: THERMOSTAT_FAN_MODE_AUTO,
FAN_HIGH: THERMOSTAT_FAN_MODE_HIGH,
FAN_MEDIUM: THERMOSTAT_FAN_MODE_MEDIUM,
FAN_LOW: THERMOSTAT_FAN_MODE_LOW,
FAN_ON: THERMOSTAT_FAN_MODE_ON,
FAN_OFF: THERMOSTAT_FAN_MODE_OFF,
}
DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()}
HVAC_MODE_TO_DECONZ: dict[str, str] = {
HVAC_MODE_AUTO: THERMOSTAT_MODE_AUTO,
HVAC_MODE_COOL: THERMOSTAT_MODE_COOL,
HVAC_MODE_HEAT: THERMOSTAT_MODE_HEAT,
HVAC_MODE_OFF: THERMOSTAT_MODE_OFF,
}
DECONZ_PRESET_AUTO = "auto"
DECONZ_PRESET_COMPLEX = "complex"
DECONZ_PRESET_HOLIDAY = "holiday"
DECONZ_PRESET_MANUAL = "manual"
PRESET_MODE_TO_DECONZ = {
DECONZ_PRESET_AUTO: THERMOSTAT_PRESET_AUTO,
PRESET_BOOST: THERMOSTAT_PRESET_BOOST,
PRESET_COMFORT: THERMOSTAT_PRESET_COMFORT,
DECONZ_PRESET_COMPLEX: THERMOSTAT_PRESET_COMPLEX,
PRESET_ECO: THERMOSTAT_PRESET_ECO,
DECONZ_PRESET_HOLIDAY: THERMOSTAT_PRESET_HOLIDAY,
DECONZ_PRESET_MANUAL: THERMOSTAT_PRESET_MANUAL,
}
DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()}
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.
"""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
def async_add_climate(sensors: list[Thermostat] | None = None) -> None:
"""Add climate devices from deCONZ."""
entities: list[DeconzThermostat] = []
if sensors is None:
sensors = list(gateway.api.sensors.thermostat.values())
for sensor in sensors:
if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
continue
if (
isinstance(sensor, Thermostat)
and sensor.unique_id not in gateway.entities[DOMAIN]
):
entities.append(DeconzThermostat(sensor, gateway))
if entities:
async_add_entities(entities)
config_entry.async_on_unload(
async_dispatcher_connect(
hass,
gateway.signal_new_sensor,
async_add_climate,
)
)
async_add_climate()
class DeconzThermostat(DeconzDevice, ClimateEntity):
"""Representation of a deCONZ thermostat."""
TYPE = DOMAIN
_device: Thermostat
_attr_temperature_unit = TEMP_CELSIUS
def __init__(self, device: Thermostat, gateway: DeconzGateway) -> None:
"""Set up thermostat device."""
super().__init__(device, gateway)
self.supported_hvac_modes = [
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
]
if device.mode:
self.supported_hvac_modes.append(HVAC_MODE_AUTO)
if "coolsetpoint" in device.raw["config"]:
self.supported_hvac_modes.append(HVAC_MODE_COOL)
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
if device.fan_mode:
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
if device.preset:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
# Fan control
@property
def fan_mode(self) -> str:
"""Return fan operation."""
if self._device.fan_mode in DECONZ_TO_FAN_MODE:
return DECONZ_TO_FAN_MODE[self._device.fan_mode]
return DECONZ_TO_FAN_MODE[FAN_ON if self._device.state_on else FAN_OFF]
@property
def fan_modes(self) -> list[str]:
"""Return the list of available fan operation modes."""
return list(FAN_MODE_TO_DECONZ)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
if fan_mode not in FAN_MODE_TO_DECONZ:
raise ValueError(f"Unsupported fan mode {fan_mode}")
await self._device.set_config(fan_mode=FAN_MODE_TO_DECONZ[fan_mode])
# HVAC control
@property
def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.
Need to be one of HVAC_MODE_*.
"""
if self._device.mode in self.supported_hvac_modes:
return HVAC_MODE_TO_DECONZ[self._device.mode]
return HVAC_MODE_HEAT if self._device.state_on else HVAC_MODE_OFF
@property
def hvac_modes(self) -> list[str]:
"""Return the list of available hvac operation modes."""
return self.supported_hvac_modes
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
if hvac_mode not in self.supported_hvac_modes:
raise ValueError(f"Unsupported HVAC mode {hvac_mode}")
if len(self.supported_hvac_modes) == 2: # Only allow turn on and off thermostat
await self._device.set_config(on=hvac_mode != HVAC_MODE_OFF)
else:
await self._device.set_config(mode=HVAC_MODE_TO_DECONZ[hvac_mode])
# Preset control
@property
def preset_mode(self) -> str | None:
"""Return preset mode."""
if self._device.preset in DECONZ_TO_PRESET_MODE:
return DECONZ_TO_PRESET_MODE[self._device.preset]
return None
@property
def preset_modes(self) -> list[str]:
"""Return the list of available preset modes."""
return list(PRESET_MODE_TO_DECONZ)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
if preset_mode not in PRESET_MODE_TO_DECONZ:
raise ValueError(f"Unsupported preset mode {preset_mode}")
await self._device.set_config(preset=PRESET_MODE_TO_DECONZ[preset_mode])
# Temperature control
@property
def current_temperature(self) -> float:
"""Return the current temperature."""
return self._device.scaled_temperature # type: ignore[no-any-return]
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
if self._device.mode == THERMOSTAT_MODE_COOL and self._device.cooling_setpoint:
return self._device.cooling_setpoint # type: ignore[no-any-return]
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}")
data = {"heating_setpoint": kwargs[ATTR_TEMPERATURE] * 100}
if self._device.mode == "cool":
data = {"cooling_setpoint": kwargs[ATTR_TEMPERATURE] * 100}
await self._device.set_config(**data)
@property
def extra_state_attributes(self) -> dict[str, bool | int]:
"""Return the state attributes of the thermostat."""
attr = {}
if self._device.offset is not None:
attr[ATTR_OFFSET] = self._device.offset
if self._device.valve is not None:
attr[ATTR_VALVE] = self._device.valve
if self._device.locked is not None:
attr[ATTR_LOCKED] = self._device.locked
return attr