Strict typing Sensibo (#72454)
This commit is contained in:
parent
a4f678e7c9
commit
24c34c0ef0
15 changed files with 136 additions and 40 deletions
|
@ -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.*
|
||||
|
|
|
@ -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,12 +96,15 @@ async def async_setup_entry(
|
|||
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
|
||||
|
||||
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 device_id, device_data in coordinator.data.parsed.items()
|
||||
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
|
||||
if device_data.motion_sensors
|
||||
)
|
||||
entities.extend(
|
||||
SensiboDeviceSensor(coordinator, device_id, description)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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."""
|
||||
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."""
|
||||
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."""
|
||||
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()
|
||||
|
|
|
@ -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] = {}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"]
|
||||
},
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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,12 +130,14 @@ async def async_setup_entry(
|
|||
|
||||
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
|
||||
|
||||
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 device_id, device_data in coordinator.data.parsed.items()
|
||||
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
|
||||
if device_data.motion_sensors
|
||||
)
|
||||
entities.extend(
|
||||
SensiboDeviceSensor(coordinator, device_id, description)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ from .const import DOMAIN
|
|||
from .coordinator import SensiboDataUpdateCoordinator
|
||||
from .entity import SensiboDeviceBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceBaseEntityDescriptionMixin:
|
||||
|
|
|
@ -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
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue