diff --git a/.strict-typing b/.strict-typing index e07d8b9cfc8..7fe03203583 100644 --- a/.strict-typing +++ b/.strict-typing @@ -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.* diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 27e551a51c8..e8d83f04593 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -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) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index c1e690cd28a..4b0e797a5b7 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -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() diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index c4b637e4439..a3254a01839 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -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] = {} diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ce85ecf2a38..c2f4869a4e6 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -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] diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 308d991e675..c289322d584 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -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"] }, diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index fc18e28f1a3..89bd9b270a9 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -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.""" diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index 56b8fbac4fd..f64411ff4dc 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -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]: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 7ac871a61eb..7254948bdad 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -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) diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 6e227a891b0..48304cbd3c5 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -20,6 +20,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class DeviceBaseEntityDescriptionMixin: diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py index fda9d4a210e..8a181cbe568 100644 --- a/homeassistant/components/sensibo/util.py +++ b/homeassistant/components/sensibo/util.py @@ -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 diff --git a/mypy.ini b/mypy.ini index e0c512782fb..e2159766fb4 100644 --- a/mypy.ini +++ b/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 diff --git a/requirements_all.txt b/requirements_all.txt index bf7769454f8..93c151fd843 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 698dc977f2b..718e876aa2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 0b6c043240c..79813727c15 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -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