Strict typing Sensibo (#72454)

This commit is contained in:
G Johansson 2022-05-29 01:26:50 +02:00 committed by GitHub
parent a4f678e7c9
commit 24c34c0ef0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 136 additions and 40 deletions

View file

@ -196,6 +196,7 @@ homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.samsungtv.*
homeassistant.components.scene.*
homeassistant.components.select.*
homeassistant.components.sensibo.*
homeassistant.components.sensor.*
homeassistant.components.senseme.*
homeassistant.components.senz.*

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pysensibo.model import MotionSensor, SensiboDevice
@ -20,6 +21,8 @@ from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
PARALLEL_UPDATES = 0
@dataclass
class MotionBaseEntityDescriptionMixin:
@ -93,13 +96,16 @@ async def async_setup_entry(
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
entities.extend(
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
for device_id, device_data in coordinator.data.parsed.items()
for sensor_id, sensor_data in device_data.motion_sensors.items()
for description in MOTION_SENSOR_TYPES
if device_data.motion_sensors
)
for device_id, device_data in coordinator.data.parsed.items():
if device_data.motion_sensors:
entities.extend(
SensiboMotionSensor(
coordinator, device_id, sensor_id, sensor_data, description
)
for sensor_id, sensor_data in device_data.motion_sensors.items()
for description in MOTION_SENSOR_TYPES
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for description in DEVICE_SENSOR_TYPES
@ -140,6 +146,8 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if TYPE_CHECKING:
assert self.sensor_data
return self.entity_description.value_fn(self.sensor_data)

View file

@ -1,6 +1,8 @@
"""Support for Sensibo wifi-enabled home thermostats."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@ -24,6 +26,7 @@ from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
SERVICE_ASSUME_STATE = "assume_state"
PARALLEL_UPDATES = 0
FIELD_TO_FLAG = {
"fanLevel": ClimateEntityFeature.FAN_MODE,
@ -107,70 +110,87 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation."""
if self.device_data.device_on:
if self.device_data.device_on and self.device_data.hvac_mode:
return SENSIBO_TO_HA[self.device_data.hvac_mode]
return HVACMode.OFF
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available hvac operation modes."""
return [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes]
hvac_modes = []
if TYPE_CHECKING:
assert self.device_data.hvac_modes
for mode in self.device_data.hvac_modes:
hvac_modes.append(SENSIBO_TO_HA[mode])
return hvac_modes if hvac_modes else [HVACMode.OFF]
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return convert_temperature(
self.device_data.temp,
TEMP_CELSIUS,
self.temperature_unit,
)
if self.device_data.temp:
return convert_temperature(
self.device_data.temp,
TEMP_CELSIUS,
self.temperature_unit,
)
return None
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self.device_data.target_temp
target_temp: int | None = self.device_data.target_temp
return target_temp
@property
def target_temperature_step(self) -> float | None:
"""Return the supported step of target temperature."""
return self.device_data.temp_step
target_temp_step: int = self.device_data.temp_step
return target_temp_step
@property
def fan_mode(self) -> str | None:
"""Return the fan setting."""
return self.device_data.fan_mode
fan_mode: str | None = self.device_data.fan_mode
return fan_mode
@property
def fan_modes(self) -> list[str] | None:
"""Return the list of available fan modes."""
return self.device_data.fan_modes
if self.device_data.fan_modes:
return self.device_data.fan_modes
return None
@property
def swing_mode(self) -> str | None:
"""Return the swing setting."""
return self.device_data.swing_mode
swing_mode: str | None = self.device_data.swing_mode
return swing_mode
@property
def swing_modes(self) -> list[str] | None:
"""Return the list of available swing modes."""
return self.device_data.swing_modes
if self.device_data.swing_modes:
return self.device_data.swing_modes
return None
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
return self.device_data.temp_list[0]
min_temp: int = self.device_data.temp_list[0]
return min_temp
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
return self.device_data.temp_list[-1]
max_temp: int = self.device_data.temp_list[-1]
return max_temp
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.device_data.available and super().available
async def async_set_temperature(self, **kwargs) -> None:
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if "targetTemperature" not in self.device_data.active_features:
raise HomeAssistantError(
@ -255,7 +275,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
f"Could not set state for device {self.name} due to reason {failure}"
)
async def async_assume_state(self, state) -> None:
async def async_assume_state(self, state: str) -> None:
"""Sync state with api."""
await self._async_set_ac_state_property("on", state != HVACMode.OFF, True)
await self.coordinator.async_refresh()

View file

@ -75,7 +75,9 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_user(self, user_input=None) -> FlowResult:
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}

View file

@ -1,7 +1,7 @@
"""Base entity for Sensibo integration."""
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
import async_timeout
from pysensibo.model import MotionSensor, SensiboDevice
@ -119,6 +119,8 @@ class SensiboMotionBaseEntity(SensiboBaseEntity):
)
@property
def sensor_data(self) -> MotionSensor:
def sensor_data(self) -> MotionSensor | None:
"""Return data for device."""
if TYPE_CHECKING:
assert self.device_data.motion_sensors
return self.device_data.motion_sensors[self._sensor_id]

View file

@ -2,10 +2,11 @@
"domain": "sensibo",
"name": "Sensibo",
"documentation": "https://www.home-assistant.io/integrations/sensibo",
"requirements": ["pysensibo==1.0.14"],
"requirements": ["pysensibo==1.0.15"],
"config_flow": true,
"codeowners": ["@andrey-git", "@gjohansson-ST"],
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"homekit": {
"models": ["Sensibo"]
},

View file

@ -14,6 +14,8 @@ from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
PARALLEL_UPDATES = 0
@dataclass
class SensiboEntityDescriptionMixin:
@ -89,7 +91,8 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity):
@property
def value(self) -> float | None:
"""Return the value from coordinator data."""
return getattr(self.device_data, self.entity_description.key)
value: float | None = getattr(self.device_data, self.entity_description.key)
return value
async def async_set_value(self, value: float) -> None:
"""Set value for calibration."""

View file

@ -13,6 +13,8 @@ from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
PARALLEL_UPDATES = 0
@dataclass
class SensiboSelectDescriptionMixin:
@ -82,7 +84,10 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity):
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
return getattr(self.device_data, self.entity_description.remote_key)
option: str | None = getattr(
self.device_data, self.entity_description.remote_key
)
return option
@property
def options(self) -> list[str]:

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pysensibo.model import MotionSensor, SensiboDevice
@ -29,6 +30,8 @@ from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
PARALLEL_UPDATES = 0
@dataclass
class MotionBaseEntityDescriptionMixin:
@ -127,13 +130,15 @@ async def async_setup_entry(
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
entities.extend(
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
for device_id, device_data in coordinator.data.parsed.items()
for sensor_id, sensor_data in device_data.motion_sensors.items()
for description in MOTION_SENSOR_TYPES
if device_data.motion_sensors
)
for device_id, device_data in coordinator.data.parsed.items():
if device_data.motion_sensors:
entities.extend(
SensiboMotionSensor(
coordinator, device_id, sensor_id, sensor_data, description
)
for sensor_id, sensor_data in device_data.motion_sensors.items()
for description in MOTION_SENSOR_TYPES
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for device_id, device_data in coordinator.data.parsed.items()
@ -173,6 +178,8 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
if TYPE_CHECKING:
assert self.sensor_data
return self.entity_description.value_fn(self.sensor_data)

View file

@ -20,6 +20,8 @@ from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
PARALLEL_UPDATES = 0
@dataclass
class DeviceBaseEntityDescriptionMixin:

View file

@ -31,7 +31,7 @@ async def async_validate_api(hass: HomeAssistant, api_key: str) -> str:
raise ConnectionError from err
devices = device_query["result"]
user = user_query["result"].get("username")
user: str = user_query["result"].get("username")
if not devices:
LOGGER.error("Could not retrieve any devices from Sensibo servers")
raise NoDevicesError

View file

@ -1919,6 +1919,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.sensibo.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.sensor.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View file

@ -1792,7 +1792,7 @@ pysaj==0.0.16
pysdcp==1
# homeassistant.components.sensibo
pysensibo==1.0.14
pysensibo==1.0.15
# homeassistant.components.serial
# homeassistant.components.zha

View file

@ -1211,7 +1211,7 @@ pyruckus==0.12
pysabnzbd==1.1.1
# homeassistant.components.sensibo
pysensibo==1.0.14
pysensibo==1.0.15
# homeassistant.components.serial
# homeassistant.components.zha

View file

@ -624,3 +624,37 @@ async def test_climate_assumed_state(
state2 = hass.states.get("climate.hallway")
assert state2.state == "off"
async def test_climate_no_fan_no_swing(
hass: HomeAssistant,
load_int: ConfigEntry,
monkeypatch: pytest.MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the Sensibo climate fan service."""
state = hass.states.get("climate.hallway")
assert state.attributes["fan_mode"] == "high"
assert state.attributes["swing_mode"] == "stopped"
monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", None)
monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", None)
monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_modes", None)
monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_modes", None)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
):
async_fire_time_changed(
hass,
dt.utcnow() + timedelta(minutes=5),
)
await hass.async_block_till_done()
state = hass.states.get("climate.hallway")
assert state.attributes["fan_mode"] is None
assert state.attributes["swing_mode"] is None
assert state.attributes["fan_modes"] is None
assert state.attributes["swing_modes"] is None