diff --git a/.strict-typing b/.strict-typing index f663ec6a6b9..3eae62f4b4a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -151,6 +151,7 @@ homeassistant.components.water_heater.* homeassistant.components.watttime.* homeassistant.components.weather.* homeassistant.components.websocket_api.* +homeassistant.components.wemo.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 27d3a0cbf25..24cf7d66a0a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,7 +1,9 @@ """Support for WeMo device discovery.""" from __future__ import annotations +from collections.abc import Sequence import logging +from typing import Any, Optional, Tuple import pywemo import voluptuous as vol @@ -14,10 +16,11 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISCOVERY, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN @@ -44,21 +47,20 @@ WEMO_MODEL_DISPATCH = { _LOGGER = logging.getLogger(__name__) +HostPortTuple = Tuple[str, Optional[int]] -def coerce_host_port(value): + +def coerce_host_port(value: str) -> HostPortTuple: """Validate that provided value is either just host or host:port. Returns (host, None) or (host, port) respectively. """ - host, _, port = value.partition(":") + host, _, port_str = value.partition(":") if not host: raise vol.Invalid("host cannot be empty") - if port: - port = cv.port(port) - else: - port = None + port = cv.port(port_str) if port_str else None return host, port @@ -82,7 +84,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for WeMo devices.""" hass.data[DOMAIN] = { "config": config.get(DOMAIN, {}), @@ -112,11 +114,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: discovery_responder = pywemo.ssdp.DiscoveryResponder(registry.port) await hass.async_add_executor_job(discovery_responder.start) - static_conf = config.get(CONF_STATIC, []) + static_conf: Sequence[HostPortTuple] = config.get(CONF_STATIC, []) wemo_dispatcher = WemoDispatcher(entry) wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) - async def async_stop_wemo(event): + async def async_stop_wemo(event: Event) -> None: """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") await hass.async_add_executor_job(registry.stop) @@ -142,8 +144,8 @@ class WemoDispatcher: def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the WemoDispatcher.""" self._config_entry = config_entry - self._added_serial_numbers = set() - self._loaded_components = set() + self._added_serial_numbers: set[str] = set() + self._loaded_components: set[str] = set() async def async_add_unique_device( self, hass: HomeAssistant, wemo: pywemo.WeMoDevice @@ -191,16 +193,16 @@ class WemoDiscovery: self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher, - static_config: list[tuple[[str, str | None]]], + static_config: Sequence[HostPortTuple], ) -> None: """Initialize the WemoDiscovery.""" self._hass = hass self._wemo_dispatcher = wemo_dispatcher - self._stop = None + self._stop: CALLBACK_TYPE | None = None self._scan_delay = 0 self._static_config = static_config - async def async_discover_and_schedule(self, *_) -> None: + async def async_discover_and_schedule(self, *_: tuple[Any]) -> None: """Periodically scan the network looking for WeMo devices.""" _LOGGER.debug("Scanning network for WeMo devices") try: @@ -229,26 +231,23 @@ class WemoDiscovery: self._stop() self._stop = None - async def discover_statics(self): + async def discover_statics(self) -> None: """Initialize or Re-Initialize connections to statically configured devices.""" - if self._static_config: - _LOGGER.debug("Adding statically configured WeMo devices") - for device in await gather_with_concurrency( - MAX_CONCURRENCY, - *( - self._hass.async_add_executor_job( - validate_static_config, host, port - ) - for host, port in self._static_config - ), - ): - if device: - await self._wemo_dispatcher.async_add_unique_device( - self._hass, device - ) + if not self._static_config: + return + _LOGGER.debug("Adding statically configured WeMo devices") + for device in await gather_with_concurrency( + MAX_CONCURRENCY, + *( + self._hass.async_add_executor_job(validate_static_config, host, port) + for host, port in self._static_config + ), + ): + if device: + await self._wemo_dispatcher.async_add_unique_device(self._hass, device) -def validate_static_config(host, port): +def validate_static_config(host: str, port: int | None) -> pywemo.WeMoDevice | None: """Handle a static config.""" url = pywemo.setup_url_for_address(host, port) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 1341d5526a3..766ca61c560 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -4,16 +4,24 @@ import asyncio from pywemo import Insight, Maker from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity +from .wemo_device import DeviceCoordinator -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 WeMo binary sensors.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" if isinstance(coordinator.wemo, Insight): async_add_entities([InsightBinarySensor(coordinator)]) @@ -38,7 +46,7 @@ class WemoBinarySensor(WemoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the state is on. Standby is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) class MakerBinarySensor(WemoEntity, BinarySensorEntity): @@ -49,7 +57,7 @@ class MakerBinarySensor(WemoEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the Maker's sensor is pulled low.""" - return self.wemo.has_sensor and self.wemo.sensor_state == 0 + return bool(self.wemo.has_sensor) and self.wemo.sensor_state == 0 class InsightBinarySensor(WemoBinarySensor): diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index b778779ea3c..6783d870cdc 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,12 +2,13 @@ import pywemo +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow from . import DOMAIN -async def _async_has_devices(hass): +async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" return bool(await hass.async_add_executor_job(pywemo.discover_devices)) diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index da9a157e1a4..1cdc29fa995 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -1,10 +1,20 @@ """Triggers for WeMo devices.""" +from __future__ import annotations + +from typing import Any + from pywemo.subscribe import EVENT_TYPE_LONG_PRESS import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT from .wemo_device import async_get_coordinator @@ -18,7 +28,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: """Return a list of triggers.""" wemo_trigger = { @@ -44,7 +56,12 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( { diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 2811d371f6b..824fcacae7c 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -33,7 +33,7 @@ class WemoEntity(CoordinatorEntity): self._available = True @property - def name_suffix(self): + def name_suffix(self) -> str | None: """Suffix to append to the WeMo device name.""" return self._name_suffix @@ -42,7 +42,7 @@ class WemoEntity(CoordinatorEntity): """Return the name of the device if any.""" if suffix := self.name_suffix: return f"{self.wemo.name} {suffix}" - return self.wemo.name + return str(self.wemo.name) @property def available(self) -> bool: @@ -50,10 +50,10 @@ class WemoEntity(CoordinatorEntity): return super().available and self._available @property - def unique_id_suffix(self): + def unique_id_suffix(self) -> str | None: """Suffix to append to the WeMo device's unique ID.""" if self._unique_id_suffix is None and self.name_suffix is not None: - return self._name_suffix.lower() + return self.name_suffix.lower() return self._unique_id_suffix @property @@ -61,7 +61,7 @@ class WemoEntity(CoordinatorEntity): """Return the id of this WeMo device.""" if suffix := self.unique_id_suffix: return f"{self.wemo.serialnumber}_{suffix}" - return self.wemo.serialnumber + return str(self.wemo.serialnumber) @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 00f9b77aa61..003dfa7e633 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,14 +1,19 @@ """Support for WeMo humidifier.""" +from __future__ import annotations + import asyncio from datetime import timedelta import math +from typing import Any import voluptuous as vol from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -21,6 +26,7 @@ from .const import ( SERVICE_SET_HUMIDITY, ) from .entity import WemoEntity +from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -63,10 +69,14 @@ SET_HUMIDITY_SCHEMA = { } -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 WeMo binary sensors.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoHumidifier(coordinator)]) @@ -95,7 +105,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class WemoHumidifier(WemoEntity, FanEntity): """Representation of a WeMo humidifier.""" - def __init__(self, coordinator): + def __init__(self, coordinator: DeviceCoordinator) -> None: """Initialize the WeMo switch.""" super().__init__(coordinator) if self.wemo.fan_mode != WEMO_FAN_OFF: @@ -104,12 +114,12 @@ class WemoHumidifier(WemoEntity, FanEntity): self._last_fan_on_mode = WEMO_FAN_MEDIUM @property - def icon(self): + def icon(self) -> str: """Return the icon of device based on its type.""" return "mdi:water-percent" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { ATTR_CURRENT_HUMIDITY: self.wemo.current_humidity_percent, @@ -145,26 +155,26 @@ class WemoHumidifier(WemoEntity, FanEntity): @property def is_on(self) -> bool: """Return true if the state is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) def turn_on( self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn the fan on.""" self.set_percentage(percentage) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" with self._wemo_exception_handler("turn off"): self.wemo.set_state(WEMO_FAN_OFF) self.schedule_update_ha_state() - def set_percentage(self, percentage: int) -> None: + def set_percentage(self, percentage: int | None) -> None: """Set the fan_mode of the Humidifier.""" if percentage is None: named_speed = self._last_fan_on_mode diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index c46a4e78440..eb19355f2b3 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,5 +1,8 @@ """Support for Belkin WeMo lights.""" +from __future__ import annotations + import asyncio +from typing import Any from pywemo.ouimeaux_device import bridge @@ -14,10 +17,12 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN @@ -32,10 +37,14 @@ SUPPORT_WEMO = ( WEMO_OFF = 0 -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 WeMo lights.""" - async def _discovered_wemo(coordinator: DeviceCoordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" if isinstance(coordinator.wemo, bridge.Bridge): async_setup_bridge(hass, config_entry, async_add_entities, coordinator) @@ -53,12 +62,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def async_setup_bridge(hass, config_entry, async_add_entities, coordinator): +def async_setup_bridge( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + coordinator: DeviceCoordinator, +) -> None: """Set up a WeMo link.""" known_light_ids = set() @callback - def async_update_lights(): + def async_update_lights() -> None: """Check to see if the bridge has any new lights.""" new_lights = [] @@ -87,7 +101,7 @@ class WemoLight(WemoEntity, LightEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return self.light.name + return str(self.light.name) @property def available(self) -> bool: @@ -95,9 +109,9 @@ class WemoLight(WemoEntity, LightEntity): return super().available and self.light.state.get("available") @property - def unique_id(self): + def unique_id(self) -> str: """Return the ID of this light.""" - return self.light.uniqueID + return str(self.light.uniqueID) @property def device_info(self) -> DeviceInfo: @@ -111,33 +125,35 @@ class WemoLight(WemoEntity, LightEntity): ) @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return self.light.state.get("level", 255) + return int(self.light.state.get("level", 255)) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs color values of this light.""" if xy_color := self.light.state.get("color_xy"): return color_util.color_xy_to_hs(*xy_color) return None @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature of this light in mireds.""" - return self.light.state.get("temperature_mireds") + if (temp := self.light.state.get("temperature_mireds")) is not None: + return int(temp) + return None @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self.light.state.get("onoff") != WEMO_OFF + return bool(self.light.state.get("onoff") != WEMO_OFF) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_WEMO - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" xy_color = None @@ -168,7 +184,7 @@ class WemoLight(WemoEntity, LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) @@ -182,12 +198,12 @@ class WemoDimmer(WemoEntity, LightEntity): """Representation of a WeMo dimmer.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 1 and 100.""" wemo_brightness = int(self.wemo.get_brightness()) return int((wemo_brightness * 255) / 100) @@ -195,9 +211,9 @@ class WemoDimmer(WemoEntity, LightEntity): @property def is_on(self) -> bool: """Return true if the state is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the dimmer on.""" # Wemo dimmer switches use a range of [0, 100] to control # brightness. Level 255 might mean to set it to previous value @@ -212,7 +228,7 @@ class WemoDimmer(WemoEntity, LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the dimmer off.""" with self._wemo_exception_handler("turn off"): self.wemo.off() diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index 49e6b187515..c46e8928a03 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -7,8 +7,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util import convert @@ -17,10 +20,14 @@ from .entity import WemoEntity from .wemo_device import DeviceCoordinator -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 WeMo sensors.""" - async def _discovered_wemo(coordinator: DeviceCoordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities( [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)] @@ -42,7 +49,7 @@ class InsightSensor(WemoEntity, SensorEntity): @property def name_suffix(self) -> str: """Return the name of the entity if any.""" - return self.entity_description.name + return str(self.entity_description.name) @property def unique_id_suffix(self) -> str: @@ -50,7 +57,7 @@ class InsightSensor(WemoEntity, SensorEntity): return self.entity_description.key @property - def available(self) -> str: + def available(self) -> bool: """Return true if sensor is available.""" return ( self.entity_description.key in self.wemo.insight_params @@ -72,12 +79,11 @@ class InsightCurrentPower(InsightSensor): @property def native_value(self) -> StateType: """Return the current power consumption.""" - return ( - convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - / 1000.0 + milliwatts = convert( + self.wemo.insight_params.get(self.entity_description.key), float, 0.0 ) + assert isinstance(milliwatts, float) + return milliwatts / 1000.0 class InsightTodayEnergy(InsightSensor): @@ -94,7 +100,8 @@ class InsightTodayEnergy(InsightSensor): @property def native_value(self) -> StateType: """Return the current energy use today.""" - miliwatts = convert( + milliwatt_seconds = convert( self.wemo.insight_params.get(self.entity_description.key), float, 0.0 ) - return round(miliwatts / (1000.0 * 1000.0 * 60), 2) + assert isinstance(milliwatt_seconds, float) + return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index d1240d034b5..39a6219b98b 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,16 +1,23 @@ """Support for WeMo switches.""" +from __future__ import annotations + import asyncio from datetime import datetime, timedelta +from typing import Any from pywemo import CoffeeMaker, Insight, Maker from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity +from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -29,10 +36,14 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -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 WeMo switches.""" - async def _discovered_wemo(coordinator): + async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities([WemoSwitch(coordinator)]) @@ -50,9 +61,9 @@ class WemoSwitch(WemoEntity, SwitchEntity): """Representation of a WeMo switch.""" @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" - attr = {} + attr: dict[str, Any] = {} if isinstance(self.wemo, Maker): # Is the maker sensor on or off. if self.wemo.maker_params["hassensor"]: @@ -81,10 +92,11 @@ class WemoSwitch(WemoEntity, SwitchEntity): attr["on_total_time"] = WemoSwitch.as_uptime( self.wemo.insight_params.get("ontotal", 0) ) - attr["power_threshold_w"] = ( - convert(self.wemo.insight_params.get("powerthreshold"), float, 0.0) - / 1000.0 + threshold = convert( + self.wemo.insight_params.get("powerthreshold"), float, 0.0 ) + assert isinstance(threshold, float) + attr["power_threshold_w"] = threshold / 1000.0 if isinstance(self.wemo, CoffeeMaker): attr[ATTR_COFFEMAKER_MODE] = self.wemo.mode @@ -92,7 +104,7 @@ class WemoSwitch(WemoEntity, SwitchEntity): return attr @staticmethod - def as_uptime(_seconds): + def as_uptime(_seconds: int) -> str: """Format seconds into uptime string in the format: 00d 00h 00m 00s.""" uptime = datetime(1, 1, 1) + timedelta(seconds=_seconds) return "{:0>2d}d {:0>2d}h {:0>2d}m {:0>2d}s".format( @@ -100,26 +112,28 @@ class WemoSwitch(WemoEntity, SwitchEntity): ) @property - def current_power_w(self): + def current_power_w(self) -> float | None: """Return the current power usage in W.""" - if isinstance(self.wemo, Insight): - return ( - convert(self.wemo.insight_params.get("currentpower"), float, 0.0) - / 1000.0 - ) + if not isinstance(self.wemo, Insight): + return None + milliwatts = convert(self.wemo.insight_params.get("currentpower"), float, 0.0) + assert isinstance(milliwatts, float) + return milliwatts / 1000.0 @property - def today_energy_kwh(self): + def today_energy_kwh(self) -> float | None: """Return the today total energy usage in kWh.""" - if isinstance(self.wemo, Insight): - miliwatts = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) - return round(miliwatts / (1000.0 * 1000.0 * 60), 2) + if not isinstance(self.wemo, Insight): + return None + milliwatt_seconds = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) + assert isinstance(milliwatt_seconds, float) + return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) @property - def detail_state(self): + def detail_state(self) -> str: """Return the state of the device.""" if isinstance(self.wemo, CoffeeMaker): - return self.wemo.mode_string + return str(self.wemo.mode_string) if isinstance(self.wemo, Insight): standby_state = int(self.wemo.insight_params.get("state", 0)) if standby_state == WEMO_ON: @@ -129,9 +143,10 @@ class WemoSwitch(WemoEntity, SwitchEntity): if standby_state == WEMO_STANDBY: return STATE_STANDBY return STATE_UNKNOWN + assert False # Unreachable code statement. @property - def icon(self): + def icon(self) -> str | None: """Return the icon of device based on its type.""" if isinstance(self.wemo, CoffeeMaker): return "mdi:coffee" @@ -140,16 +155,16 @@ class WemoSwitch(WemoEntity, SwitchEntity): @property def is_on(self) -> bool: """Return true if the state is on. Standby is on.""" - return self.wemo.get_state() + return bool(self.wemo.get_state()) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" with self._wemo_exception_handler("turn on"): self.wemo.on() self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" with self._wemo_exception_handler("turn off"): self.wemo.off() diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index a507338a4cd..b7138cb0a94 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -165,4 +165,5 @@ async def async_register_device( @callback def async_get_coordinator(hass: HomeAssistant, device_id: str) -> DeviceCoordinator: """Return DeviceCoordinator for device_id.""" - return hass.data[DOMAIN]["devices"][device_id] + coordinator: DeviceCoordinator = hass.data[DOMAIN]["devices"][device_id] + return coordinator diff --git a/mypy.ini b/mypy.ini index 475840c3b4d..b4c6c802d04 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1672,6 +1672,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.wemo.*] +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.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2066,9 +2077,6 @@ ignore_errors = true [mypy-homeassistant.components.vizio.*] ignore_errors = true -[mypy-homeassistant.components.wemo.*] -ignore_errors = true - [mypy-homeassistant.components.withings.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6fc6c0e3993..220837fd10b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -127,7 +127,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.vera.*", "homeassistant.components.verisure.*", "homeassistant.components.vizio.*", - "homeassistant.components.wemo.*", "homeassistant.components.withings.*", "homeassistant.components.xbox.*", "homeassistant.components.xiaomi_aqara.*", diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index b00cfe30ef7..9f622a22c1d 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -8,7 +8,7 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ATTR_COLOR_TEMP, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -88,13 +88,16 @@ async def test_light_update_entity( # On state. pywemo_bridge_light.state["onoff"] = 1 + pywemo_bridge_light.state["temperature_mireds"] = 432 await hass.services.async_call( HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, blocking=True, ) - assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + state = hass.states.get(wemo_entity.entity_id) + assert state.attributes.get(ATTR_COLOR_TEMP) == 432 + assert state.state == STATE_ON # Off state. pywemo_bridge_light.state["onoff"] = 0