diff --git a/.strict-typing b/.strict-typing index 00ce5de2b3e..cd74910415c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -95,6 +95,7 @@ homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* +homeassistant.components.isy994.* homeassistant.components.iqvia.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index b66e6d4676e..ba152c5d840 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr @@ -98,10 +98,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_find_matching_config_entry(hass): +def _async_find_matching_config_entry( + hass: HomeAssistant, +) -> config_entries.ConfigEntry | None: for entry in hass.config_entries.async_entries(DOMAIN): if entry.source == config_entries.SOURCE_IMPORT: return entry + return None async def async_setup_entry( @@ -147,7 +150,7 @@ async def async_setup_entry( https = False port = host.port or 80 session = aiohttp_client.async_create_clientsession( - hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) elif host.scheme == "https": https = True @@ -206,7 +209,7 @@ async def async_setup_entry( hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback - def _async_stop_auto_update(event) -> None: + def _async_stop_auto_update(event: Event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() @@ -235,7 +238,7 @@ async def _async_update_listener( @callback def _async_import_options_from_data_if_missing( hass: HomeAssistant, entry: config_entries.ConfigEntry -): +) -> None: options = dict(entry.options) modified = False for importable_option in ( @@ -261,7 +264,7 @@ def _async_isy_to_configuration_url(isy: ISY) -> str: @callback def _async_get_or_create_isy_device_in_registry( - hass: HomeAssistant, entry: config_entries.ConfigEntry, isy + hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY ) -> None: device_registry = dr.async_get(hass) url = _async_isy_to_configuration_url(isy) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 5cf7d6b2b76..23e77ba849d 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,7 +1,8 @@ """Support for ISY994 binary sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta +from typing import Any from pyisy.constants import ( CMD_OFF, @@ -10,6 +11,7 @@ from pyisy.constants import ( PROTO_INSTEON, PROTO_ZWAVE, ) +from pyisy.helpers import NodeProperty from pyisy.nodes import Group, Node from homeassistant.components.binary_sensor import ( @@ -18,7 +20,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -55,12 +57,25 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY994 binary sensor platform.""" - devices = [] - devices_by_address = {} - child_nodes = [] + entities: list[ + ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity + ] = [] + entities_by_address: dict[ + str, + ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity, + ] = {} + child_nodes: list[tuple[Node, str | None, str | None]] = [] + entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]: + assert isinstance(node, Node) device_class, device_type = _detect_device_type_and_class(node) if node.protocol == PROTO_INSTEON: if node.parent_node is not None: @@ -68,38 +83,38 @@ async def async_setup_entry( # nodes have been processed child_nodes.append((node, device_class, device_type)) continue - device = ISYInsteonBinarySensorEntity(node, device_class) + entity = ISYInsteonBinarySensorEntity(node, device_class) else: - device = ISYBinarySensorEntity(node, device_class) - devices.append(device) - devices_by_address[node.address] = device + entity = ISYBinarySensorEntity(node, device_class) + entities.append(entity) + entities_by_address[node.address] = entity # Handle some special child node cases for Insteon Devices for (node, device_class, device_type) in child_nodes: subnode_id = int(node.address.split(" ")[-1], 16) # Handle Insteon Thermostats - if device_type.startswith(TYPE_CATEGORY_CLIMATE): + if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE): if subnode_id == SUBNODE_CLIMATE_COOL: # Subnode 2 is the "Cool Control" sensor # It never reports its state until first use is # detected after an ISY Restart, so we assume it's off. # As soon as the ISY Event Stream connects if it has a # valid state, it will be set. - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.COLD, False ) - devices.append(device) + entities.append(entity) elif subnode_id == SUBNODE_CLIMATE_HEAT: # Subnode 3 is the "Heat Control" sensor - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.HEAT, False ) - devices.append(device) + entities.append(entity) continue if device_class in DEVICE_PARENT_REQUIRED: - parent_device = devices_by_address.get(node.parent_node.address) - if not parent_device: + parent_entity = entities_by_address.get(node.parent_node.address) + if not parent_entity: _LOGGER.error( "Node %s has a parent node %s, but no device " "was created for the parent. Skipping", @@ -115,13 +130,15 @@ async def async_setup_entry( # These sensors use an optional "negative" subnode 2 to # snag all state changes if subnode_id == SUBNODE_NEGATIVE: - parent_device.add_negative_node(node) + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) + parent_entity.add_negative_node(node) elif subnode_id == SUBNODE_HEARTBEAT: + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) # Subnode 4 is the heartbeat node, which we will # represent as a separate binary_sensor - device = ISYBinarySensorHeartbeat(node, parent_device) - parent_device.add_heartbeat_device(device) - devices.append(device) + entity = ISYBinarySensorHeartbeat(node, parent_entity) + parent_entity.add_heartbeat_device(entity) + entities.append(entity) continue if ( device_class == BinarySensorDeviceClass.MOTION @@ -133,48 +150,49 @@ async def async_setup_entry( # the initial state is forced "OFF"/"NORMAL" if the # parent device has a valid state. This is corrected # upon connection to the ISY event stream if subnode has a valid state. - initial_state = None if parent_device.state is None else False + assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) + initial_state = None if parent_entity.state is None else False if subnode_id == SUBNODE_DUSK_DAWN: # Subnode 2 is the Dusk/Dawn sensor - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.LIGHT ) - devices.append(device) + entities.append(entity) continue if subnode_id == SUBNODE_LOW_BATTERY: # Subnode 3 is the low battery node - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.BATTERY, initial_state ) - devices.append(device) + entities.append(entity) continue if subnode_id in SUBNODE_TAMPER: # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # reported as "10", which translate from Hex to 10 and 16 resp. - device = ISYInsteonBinarySensorEntity( + entity = ISYInsteonBinarySensorEntity( node, BinarySensorDeviceClass.PROBLEM, initial_state ) - devices.append(device) + entities.append(entity) continue if subnode_id in SUBNODE_MOTION_DISABLED: # Motion Disabled Sub-node for MS II ("D" or "13") - device = ISYInsteonBinarySensorEntity(node) - devices.append(device) + entity = ISYInsteonBinarySensorEntity(node) + entities.append(entity) continue # We don't yet have any special logic for other sensor # types, so add the nodes as individual devices - device = ISYBinarySensorEntity(node, device_class) - devices.append(device) + entity = ISYBinarySensorEntity(node, device_class) + entities.append(entity) for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: - devices.append(ISYBinarySensorProgramEntity(name, status)) + entities.append(ISYBinarySensorProgramEntity(name, status)) - await migrate_old_unique_ids(hass, BINARY_SENSOR, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, BINARY_SENSOR, entities) + async_add_entities(entities) -def _detect_device_type_and_class(node: Group | Node) -> (str, str): +def _detect_device_type_and_class(node: Group | Node) -> tuple[str | None, str | None]: try: device_type = node.type except AttributeError: @@ -199,20 +217,25 @@ def _detect_device_type_and_class(node: Group | Node) -> (str, str): class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): """Representation of a basic ISY994 binary sensor device.""" - def __init__(self, node, force_device_class=None, unknown_state=None) -> None: + def __init__( + self, + node: Node, + force_device_class: str | None = None, + unknown_state: bool | None = None, + ) -> None: """Initialize the ISY994 binary sensor device.""" super().__init__(node) self._device_class = force_device_class @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 binary sensor device is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) @property - def device_class(self) -> str: + def device_class(self) -> str | None: """Return the class of this device. This was discovered by parsing the device type code during init @@ -229,11 +252,16 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): Assistant entity and handles both ways that ISY binary sensors can work. """ - def __init__(self, node, force_device_class=None, unknown_state=None) -> None: + def __init__( + self, + node: Node, + force_device_class: str | None = None, + unknown_state: bool | None = None, + ) -> None: """Initialize the ISY994 binary sensor device.""" super().__init__(node, force_device_class) - self._negative_node = None - self._heartbeat_device = None + self._negative_node: Node | None = None + self._heartbeat_device: ISYBinarySensorHeartbeat | None = None if self._node.status == ISY_VALUE_UNKNOWN: self._computed_state = unknown_state self._status_was_unknown = True @@ -252,21 +280,21 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self._async_negative_node_control_handler ) - def add_heartbeat_device(self, device) -> None: + def add_heartbeat_device(self, entity: ISYBinarySensorHeartbeat | None) -> None: """Register a heartbeat device for this sensor. The heartbeat node beats on its own, but we can gain a little reliability by considering any node activity for this sensor to be a heartbeat as well. """ - self._heartbeat_device = device + self._heartbeat_device = entity def _async_heartbeat(self) -> None: """Send a heartbeat to our heartbeat device, if we have one.""" if self._heartbeat_device is not None: self._heartbeat_device.async_heartbeat() - def add_negative_node(self, child) -> None: + def add_negative_node(self, child: Node) -> None: """Add a negative node to this binary sensor device. The negative node is a node that can receive the 'off' events @@ -287,7 +315,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self._computed_state = None @callback - def _async_negative_node_control_handler(self, event: object) -> None: + def _async_negative_node_control_handler(self, event: NodeProperty) -> None: """Handle an "On" control event from the "negative" node.""" if event.control == CMD_ON: _LOGGER.debug( @@ -299,7 +327,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self._async_heartbeat() @callback - def _async_positive_node_control_handler(self, event: object) -> None: + def _async_positive_node_control_handler(self, event: NodeProperty) -> None: """Handle On and Off control event coming from the primary node. Depending on device configuration, sometimes only On events @@ -324,7 +352,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self._async_heartbeat() @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Primary node status updates. We MOSTLY ignore these updates, as we listen directly to the Control @@ -341,7 +369,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): self._async_heartbeat() @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 binary sensor device is on. Insteon leak sensors set their primary node to On when the state is @@ -361,7 +389,14 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """Representation of the battery state of an ISY994 sensor.""" - def __init__(self, node, parent_device) -> None: + def __init__( + self, + node: Node, + parent_device: ISYInsteonBinarySensorEntity + | ISYBinarySensorEntity + | ISYBinarySensorHeartbeat + | ISYBinarySensorProgramEntity, + ) -> None: """Initialize the ISY994 binary sensor device. Computed state is set to UNKNOWN unless the ISY provided a valid @@ -372,8 +407,8 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """ super().__init__(node) self._parent_device = parent_device - self._heartbeat_timer = None - self._computed_state = None + self._heartbeat_timer: CALLBACK_TYPE | None = None + self._computed_state: bool | None = None if self.state is None: self._computed_state = False @@ -386,7 +421,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): # Start the timer on bootup, so we can change from UNKNOWN to OFF self._restart_timer() - def _heartbeat_node_control_handler(self, event: object) -> None: + def _heartbeat_node_control_handler(self, event: NodeProperty) -> None: """Update the heartbeat timestamp when any ON/OFF event is sent. The ISY uses both DON and DOF commands (alternating) for a heartbeat. @@ -395,7 +430,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): self.async_heartbeat() @callback - def async_heartbeat(self): + def async_heartbeat(self) -> None: """Mark the device as online, and restart the 25 hour timer. This gets called when the heartbeat node beats, but also when the @@ -407,17 +442,14 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): self._restart_timer() self.async_write_ha_state() - def _restart_timer(self): + def _restart_timer(self) -> None: """Restart the 25 hour timer.""" - try: + if self._heartbeat_timer is not None: self._heartbeat_timer() self._heartbeat_timer = None - except TypeError: - # No heartbeat timer is active - pass @callback - def timer_elapsed(now) -> None: + def timer_elapsed(now: datetime) -> None: """Heartbeat missed; set state to ON to indicate dead battery.""" self._computed_state = True self._heartbeat_timer = None @@ -457,7 +489,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): return BinarySensorDeviceClass.BATTERY @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Get the state attributes for the device.""" attr = super().extra_state_attributes attr["parent_entity_id"] = self._parent_device.entity_id diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 98c20bb441a..00a02e5c210 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,6 +1,8 @@ """Support for Insteon Thermostats via ISY994 Platform.""" from __future__ import annotations +from typing import Any + from pyisy.constants import ( CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_MODE, @@ -11,6 +13,7 @@ from pyisy.constants import ( PROP_UOM, PROTO_INSTEON, ) +from pyisy.nodes import Node from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -18,9 +21,11 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE, FAN_AUTO, + FAN_OFF, FAN_ON, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -76,16 +81,15 @@ async def async_setup_entry( class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Representation of an ISY994 thermostat entity.""" - def __init__(self, node) -> None: + def __init__(self, node: Node) -> None: """Initialize the ISY Thermostat entity.""" super().__init__(node) - self._node = node self._uom = self._node.uom if isinstance(self._uom, list): self._uom = self._node.uom[0] - self._hvac_action = None - self._hvac_mode = None - self._fan_mode = None + self._hvac_action: str | None = None + self._hvac_mode: str | None = None + self._fan_mode: str | None = None self._temp_unit = None self._current_humidity = 0 self._target_temp_low = 0 @@ -97,7 +101,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return ISY_SUPPORTED_FEATURES @property - def precision(self) -> str: + def precision(self) -> float: """Return the precision of the system.""" return PRECISION_TENTHS @@ -110,6 +114,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return TEMP_CELSIUS if uom.value == UOM_ISY_FAHRENHEIT: return TEMP_FAHRENHEIT + return TEMP_FAHRENHEIT @property def current_humidity(self) -> int | None: @@ -119,10 +124,10 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return int(humidity.value) @property - def hvac_mode(self) -> str | None: + def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" if not (hvac_mode := self._node.aux_properties.get(CMD_CLIMATE_MODE)): - return None + return HVAC_MODE_OFF # Which state values used depends on the mode property's UOM: uom = hvac_mode.uom @@ -133,7 +138,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): if self._node.protocol == PROTO_INSTEON else UOM_HVAC_MODE_GENERIC ) - return UOM_TO_STATES[uom].get(hvac_mode.value) + return UOM_TO_STATES[uom].get(hvac_mode.value, HVAC_MODE_OFF) @property def hvac_modes(self) -> list[str]: @@ -186,7 +191,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1) @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return [FAN_AUTO, FAN_ON] @@ -195,10 +200,10 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current fan mode ie. auto, on.""" fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING) if not fan_mode: - return None - return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value) + return FAN_OFF + return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value, FAN_OFF) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 4e700df24cb..866ec800402 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,5 +1,8 @@ """Config flow for Universal Devices ISY994 integration.""" +from __future__ import annotations + import logging +from typing import Any from urllib.parse import urlparse, urlunparse from aiohttp import CookieJar @@ -38,7 +41,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _data_schema(schema_input): +def _data_schema(schema_input: dict[str, str]) -> vol.Schema: """Generate schema with defaults.""" return vol.Schema( { @@ -51,7 +54,9 @@ def _data_schema(schema_input): ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -65,7 +70,7 @@ async def validate_input(hass: core.HomeAssistant, data): https = False port = host.port or HTTP_PORT session = aiohttp_client.async_create_clientsession( - hass, verify_ssl=None, cookie_jar=CookieJar(unsafe=True) + hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) elif host.scheme == SCHEME_HTTPS: https = True @@ -113,18 +118,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the isy994 config flow.""" - self.discovered_conf = {} + self.discovered_conf: dict[str, str] = {} @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Handle the initial step.""" errors = {} - info = None + info: dict[str, str] = {} if user_input is not None: try: info = await validate_input(self.hass, user_input) @@ -149,11 +158,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input): + async def async_step_import( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResult: """Handle import.""" return await self.async_step_user(user_input) - async def _async_set_unique_id_or_update(self, isy_mac, ip_address, port) -> None: + async def _async_set_unique_id_or_update( + self, isy_mac: str, ip_address: str, port: int | None + ) -> None: """Abort and update the ip address on change.""" existing_entry = await self.async_set_unique_id(isy_mac) if not existing_entry: @@ -211,6 +224,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a discovered isy994.""" friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info.ssdp_location + assert isinstance(url, str) parsed_url = urlparse(url) mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] if mac.startswith(UDN_UUID_PREFIX): @@ -224,6 +238,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): elif parsed_url.scheme == SCHEME_HTTPS: port = HTTPS_PORT + assert isinstance(parsed_url.hostname, str) await self._async_set_unique_id_or_update(mac, parsed_url.hostname, port) self.discovered_conf = { @@ -242,7 +257,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 470aaa64c64..8ca1ac786f8 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -204,7 +204,7 @@ UOM_PERCENTAGE = "51" # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml # Z-Wave Categories: https://www.universal-devices.com/developers/wsdk/5.0.4/4_fam.xml -NODE_FILTERS = { +NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { Platform.BINARY_SENSOR: { FILTER_UOM: [UOM_ON_OFF], FILTER_STATES: [], diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 0fcfb30a775..f00128b6d15 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,7 @@ """Support for ISY994 covers.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN @@ -31,53 +34,53 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 cover platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][COVER]: - devices.append(ISYCoverEntity(node)) + entities.append(ISYCoverEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]: - devices.append(ISYCoverProgramEntity(name, status, actions)) + entities.append(ISYCoverProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, COVER, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, COVER, entities) + async_add_entities(entities) class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Representation of an ISY994 cover device.""" @property - def current_cover_position(self) -> int: + def current_cover_position(self) -> int | None: """Return the current cover position.""" if self._node.status == ISY_VALUE_UNKNOWN: return None if self._node.uom == UOM_8_BIT_RANGE: return round(self._node.status * 100.0 / 255.0) - return sorted((0, self._node.status, 100))[1] + return int(sorted((0, self._node.status, 100))[1]) @property - def is_closed(self) -> bool: + def is_closed(self) -> bool | None: """Get whether the ISY994 cover device is closed.""" if self._node.status == ISY_VALUE_UNKNOWN: return None - return self._node.status == 0 + return bool(self._node.status == 0) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Send the open cover command to the ISY994 cover device.""" val = 100 if self._node.uom == UOM_BARRIER else None if not await self._node.turn_on(val=val): _LOGGER.error("Unable to open the cover") - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Send the close cover command to the ISY994 cover device.""" if not await self._node.turn_off(): _LOGGER.error("Unable to close the cover") - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] if self._node.uom == UOM_8_BIT_RANGE: @@ -94,12 +97,12 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): """Get whether the ISY994 cover program is closed.""" return bool(self._node.status) - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Send the open cover command to the ISY994 cover program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to open the cover") - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Send the close cover command to the ISY994 cover program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to close the cover") diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index aca0dbf5d3d..e5db8de5872 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,6 +1,8 @@ """Representation of ISYEntity Types.""" from __future__ import annotations +from typing import Any, cast + from pyisy.constants import ( COMMAND_FRIENDLY_NAME, EMPTY_TIME, @@ -8,7 +10,9 @@ from pyisy.constants import ( PROTO_GROUP, PROTO_ZWAVE, ) -from pyisy.helpers import NodeProperty +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node +from pyisy.programs import Program from homeassistant.const import ( ATTR_IDENTIFIERS, @@ -30,14 +34,14 @@ from .const import DOMAIN class ISYEntity(Entity): """Representation of an ISY994 device.""" - _name: str = None + _name: str | None = None - def __init__(self, node) -> None: + def __init__(self, node: Node) -> None: """Initialize the insteon device.""" self._node = node - self._attrs = {} - self._change_handler = None - self._control_handler = None + self._attrs: dict[str, Any] = {} + self._change_handler: EventListener | None = None + self._control_handler: EventListener | None = None async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" @@ -49,7 +53,7 @@ class ISYEntity(Entity): ) @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Handle the update event from the ISY994 Node.""" self.async_write_ha_state() @@ -72,7 +76,7 @@ class ISYEntity(Entity): self.hass.bus.fire("isy994_control", event_data) @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: # not a device @@ -90,7 +94,6 @@ class ISYEntity(Entity): basename = node.name device_info = DeviceInfo( - identifiers={}, manufacturer="Unknown", model="Unknown", name=basename, @@ -99,25 +102,30 @@ class ISYEntity(Entity): ) if hasattr(node, "address"): - device_info[ATTR_NAME] += f" ({node.address})" + assert isinstance(node.address, str) + device_info[ATTR_NAME] = f"{basename} ({node.address})" if hasattr(node, "primary_node"): device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} # ISYv5 Device Types if hasattr(node, "node_def_id") and node.node_def_id is not None: - device_info[ATTR_MODEL] = node.node_def_id + model: str = str(node.node_def_id) # Numerical Device Type if hasattr(node, "type") and node.type is not None: - device_info[ATTR_MODEL] += f" {node.type}" + model += f" {node.type}" + device_info[ATTR_MODEL] = model if hasattr(node, "protocol"): - device_info[ATTR_MANUFACTURER] = node.protocol + model = str(device_info[ATTR_MODEL]) + manufacturer = str(node.protocol) if node.protocol == PROTO_ZWAVE: # Get extra information for Z-Wave Devices - device_info[ATTR_MANUFACTURER] += f" MfrID:{node.zwave_props.mfr_id}" - device_info[ATTR_MODEL] += ( + manufacturer += f" MfrID:{node.zwave_props.mfr_id}" + model += ( f" Type:{node.zwave_props.devtype_gen} " f"ProductTypeID:{node.zwave_props.prod_type_id} " f"ProductID:{node.zwave_props.product_id}" ) + device_info[ATTR_MANUFACTURER] = manufacturer + device_info[ATTR_MODEL] = model if hasattr(node, "folder") and node.folder is not None: device_info[ATTR_SUGGESTED_AREA] = node.folder # Note: sw_version is not exposed by the ISY for the individual devices. @@ -125,17 +133,17 @@ class ISYEntity(Entity): return device_info @property - def unique_id(self) -> str: + def unique_id(self) -> str | None: """Get the unique identifier of the device.""" if hasattr(self._node, "address"): return f"{self._node.isy.configuration['uuid']}_{self._node.address}" return None @property - def old_unique_id(self) -> str: + def old_unique_id(self) -> str | None: """Get the old unique identifier of the device.""" if hasattr(self._node, "address"): - return self._node.address + return cast(str, self._node.address) return None @property @@ -174,7 +182,7 @@ class ISYNodeEntity(ISYEntity): self._attrs.update(attr) return self._attrs - async def async_send_node_command(self, command): + async def async_send_node_command(self, command: str) -> None: """Respond to an entity service command call.""" if not hasattr(self._node, command): raise HomeAssistantError( @@ -183,8 +191,12 @@ class ISYNodeEntity(ISYEntity): await getattr(self._node, command)() async def async_send_raw_node_command( - self, command, value=None, unit_of_measurement=None, parameters=None - ): + self, + command: str, + value: Any | None = None, + unit_of_measurement: str | None = None, + parameters: Any | None = None, + ) -> None: """Respond to an entity service raw command call.""" if not hasattr(self._node, "send_cmd"): raise HomeAssistantError( @@ -192,7 +204,7 @@ class ISYNodeEntity(ISYEntity): ) await self._node.send_cmd(command, value, unit_of_measurement, parameters) - async def async_get_zwave_parameter(self, parameter): + async def async_get_zwave_parameter(self, parameter: Any) -> None: """Respond to an entity service command to request a Z-Wave device parameter from the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( @@ -200,7 +212,9 @@ class ISYNodeEntity(ISYEntity): ) await self._node.get_zwave_parameter(parameter) - async def async_set_zwave_parameter(self, parameter, value, size): + async def async_set_zwave_parameter( + self, parameter: Any, value: Any | None, size: int | None + ) -> None: """Respond to an entity service command to set a Z-Wave device parameter via the ISY.""" if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( @@ -209,7 +223,7 @@ class ISYNodeEntity(ISYEntity): await self._node.set_zwave_parameter(parameter, value, size) await self._node.get_zwave_parameter(parameter) - async def async_rename_node(self, name): + async def async_rename_node(self, name: str) -> None: """Respond to an entity service command to rename a node on the ISY.""" await self._node.rename(name) @@ -217,7 +231,7 @@ class ISYNodeEntity(ISYEntity): class ISYProgramEntity(ISYEntity): """Representation of an ISY994 program base.""" - def __init__(self, name: str, status, actions=None) -> None: + def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: """Initialize the ISY994 program-based entity.""" super().__init__(status) self._name = name diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 28d0675a184..bf4d48ad3e8 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON @@ -27,16 +28,16 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 fan platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYFanEntity | ISYFanProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][FAN]: - devices.append(ISYFanEntity(node)) + entities.append(ISYFanEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]: - devices.append(ISYFanProgramEntity(name, status, actions)) + entities.append(ISYFanProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, FAN, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, FAN, entities) + async_add_entities(entities) class ISYFanEntity(ISYNodeEntity, FanEntity): @@ -57,11 +58,11 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): return int_states_in_range(SPEED_RANGE) @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get if the fan is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None - return self._node.status != 0 + return bool(self._node.status != 0) async def async_set_percentage(self, percentage: int) -> None: """Set node to speed percentage for the ISY994 fan device.""" @@ -75,15 +76,15 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): async def async_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: """Send the turn on command to the ISY994 fan device.""" await self.async_set_percentage(percentage or 67) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 fan device.""" await self._node.turn_off() @@ -111,19 +112,19 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): @property def is_on(self) -> bool: """Get if the fan is on.""" - return self._node.status != 0 + return bool(self._node.status != 0) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn on command to ISY994 fan program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") async def async_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: """Send the turn off command to ISY994 fan program.""" if not await self._actions.run_else(): diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index d1790fcc13c..6d0a1d303bb 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,7 +1,8 @@ """Sorting helpers for ISY994 device classifications.""" from __future__ import annotations -from typing import Any +from collections.abc import Sequence +from typing import TYPE_CHECKING, cast from pyisy.constants import ( ISY_VALUE_UNKNOWN, @@ -21,6 +22,7 @@ from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get_registry @@ -53,12 +55,15 @@ from .const import ( UOM_ISYV4_DEGREES, ) +if TYPE_CHECKING: + from .entity import ISYEntity + BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_ISY_STATES = ["on", "off"] def _check_for_node_def( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the node_def_id for any platforms. @@ -81,7 +86,7 @@ def _check_for_node_def( def _check_for_insteon_type( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the Insteon type for any platforms. @@ -146,7 +151,7 @@ def _check_for_insteon_type( def _check_for_zwave_cat( - hass_isy_data: dict, node: Group | Node, single_platform: str = None + hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the ISY Z-Wave Category for any platforms. @@ -176,8 +181,8 @@ def _check_for_zwave_cat( def _check_for_uom_id( hass_isy_data: dict, node: Group | Node, - single_platform: str = None, - uom_list: list = None, + single_platform: Platform | None = None, + uom_list: list[str] | None = None, ) -> bool: """Check if a node's uom matches any of the platforms uom filter. @@ -211,8 +216,8 @@ def _check_for_uom_id( def _check_for_states_in_uom( hass_isy_data: dict, node: Group | Node, - single_platform: str = None, - states_list: list = None, + single_platform: Platform | None = None, + states_list: list[str] | None = None, ) -> bool: """Check if a list of uoms matches two possible filters. @@ -247,9 +252,11 @@ def _check_for_states_in_uom( def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass_isy_data, node, single_platform=BINARY_SENSOR): + if _check_for_node_def(hass_isy_data, node, single_platform=Platform.BINARY_SENSOR): return True - if _check_for_insteon_type(hass_isy_data, node, single_platform=BINARY_SENSOR): + if _check_for_insteon_type( + hass_isy_data, node, single_platform=Platform.BINARY_SENSOR + ): return True # For the next two checks, we're providing our own set of uoms that @@ -257,13 +264,16 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: # checks in the context of already knowing that this is definitely a # sensor device. if _check_for_uom_id( - hass_isy_data, node, single_platform=BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS + hass_isy_data, + node, + single_platform=Platform.BINARY_SENSOR, + uom_list=BINARY_SENSOR_UOMS, ): return True if _check_for_states_in_uom( hass_isy_data, node, - single_platform=BINARY_SENSOR, + single_platform=Platform.BINARY_SENSOR, states_list=BINARY_SENSOR_ISY_STATES, ): return True @@ -275,7 +285,7 @@ def _categorize_nodes( hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str ) -> None: """Sort the nodes to their proper platforms.""" - for (path, node) in nodes: + for path, node in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: # Don't import this node as a device at all @@ -365,43 +375,45 @@ def _categorize_variables( async def migrate_old_unique_ids( - hass: HomeAssistant, platform: str, devices: list[Any] | None + hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] ) -> None: """Migrate to new controller-specific unique ids.""" registry = await async_get_registry(hass) - for device in devices: + for entity in entities: + if entity.old_unique_id is None or entity.unique_id is None: + continue old_entity_id = registry.async_get_entity_id( - platform, DOMAIN, device.old_unique_id + platform, DOMAIN, entity.old_unique_id ) if old_entity_id is not None: _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", - device.old_unique_id, - device.unique_id, + entity.old_unique_id, + entity.unique_id, ) - registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id) + registry.async_update_entity(old_entity_id, new_unique_id=entity.unique_id) old_entity_id_2 = registry.async_get_entity_id( - platform, DOMAIN, device.unique_id.replace(":", "") + platform, DOMAIN, entity.unique_id.replace(":", "") ) if old_entity_id_2 is not None: _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", - device.unique_id.replace(":", ""), - device.unique_id, + entity.unique_id.replace(":", ""), + entity.unique_id, ) registry.async_update_entity( - old_entity_id_2, new_unique_id=device.unique_id + old_entity_id_2, new_unique_id=entity.unique_id ) def convert_isy_value_to_hass( value: int | float | None, - uom: str, + uom: str | None, precision: int | str, fallback_precision: int | None = None, -) -> float | int: +) -> float | int | None: """Fix ISY Reported Values. ISY provides float values as an integer and precision component. @@ -416,7 +428,7 @@ def convert_isy_value_to_hass( if uom in (UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES): return round(float(value) / 2.0, 1) if precision not in ("0", 0): - return round(float(value) / 10 ** int(precision), int(precision)) + return cast(float, round(float(value) / 10 ** int(precision), int(precision))) if fallback_precision: return round(float(value), fallback_precision) return value diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 2fd98b6f177..640442c3f19 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,7 +1,11 @@ """Support for ISY994 lights.""" from __future__ import annotations +from typing import Any + from pyisy.constants import ISY_VALUE_UNKNOWN +from pyisy.helpers import NodeProperty +from pyisy.nodes import Node from homeassistant.components.light import ( DOMAIN as LIGHT, @@ -35,22 +39,22 @@ async def async_setup_entry( isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) - devices = [] + entities = [] for node in hass_isy_data[ISY994_NODES][LIGHT]: - devices.append(ISYLightEntity(node, restore_light_state)) + entities.append(ISYLightEntity(node, restore_light_state)) - await migrate_old_unique_ids(hass, LIGHT, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, LIGHT, entities) + async_add_entities(entities) async_setup_light_services(hass) class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): """Representation of an ISY994 light device.""" - def __init__(self, node, restore_light_state) -> None: + def __init__(self, node: Node, restore_light_state: bool) -> None: """Initialize the ISY994 light device.""" super().__init__(node) - self._last_brightness = None + self._last_brightness: int | None = None self._restore_light_state = restore_light_state @property @@ -61,7 +65,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return int(self._node.status) != 0 @property - def brightness(self) -> float: + def brightness(self) -> int | None: """Get the brightness of the ISY994 light.""" if self._node.status == ISY_VALUE_UNKNOWN: return None @@ -70,14 +74,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return round(self._node.status * 255.0 / 100.0) return int(self._node.status) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 light device.""" self._last_brightness = self.brightness if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off light") @callback - def async_on_update(self, event: object) -> None: + def async_on_update(self, event: NodeProperty) -> None: """Save brightness in the update event from the ISY994 Node.""" if self._node.status not in (0, ISY_VALUE_UNKNOWN): self._last_brightness = self._node.status @@ -88,7 +92,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): super().async_on_update(event) # pylint: disable=arguments-differ - async def async_turn_on(self, brightness=None, **kwargs) -> None: + async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: """Send the turn on command to the ISY994 light device.""" if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness @@ -99,14 +103,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): _LOGGER.debug("Unable to turn on light") @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the light attributes.""" attribs = super().extra_state_attributes attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness return attribs @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS @@ -124,10 +128,10 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): ): self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] - async def async_set_on_level(self, value): + async def async_set_on_level(self, value: int) -> None: """Set the ON Level for a device.""" await self._node.set_on_level(value) - async def async_set_ramp_rate(self, value): + async def async_set_ramp_rate(self, value: int) -> None: """Set the Ramp Rate for a device.""" await self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index e2befc57487..4de5cdaa05b 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,4 +1,7 @@ """Support for ISY994 locks.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN @@ -19,33 +22,33 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 lock platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYLockEntity | ISYLockProgramEntity] = [] for node in hass_isy_data[ISY994_NODES][LOCK]: - devices.append(ISYLockEntity(node)) + entities.append(ISYLockEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]: - devices.append(ISYLockProgramEntity(name, status, actions)) + entities.append(ISYLockProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, LOCK, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, LOCK, entities) + async_add_entities(entities) class ISYLockEntity(ISYNodeEntity, LockEntity): """Representation of an ISY994 lock device.""" @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Get whether the lock is in locked state.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return VALUE_TO_STATE.get(self._node.status) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Send the lock command to the ISY994 device.""" if not await self._node.secure_lock(): _LOGGER.error("Unable to lock device") - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Send the unlock command to the ISY994 device.""" if not await self._node.secure_unlock(): _LOGGER.error("Unable to lock device") @@ -59,12 +62,12 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity): """Return true if the device is locked.""" return bool(self._node.status) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" if not await self._actions.run_then(): _LOGGER.error("Unable to lock device") - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" if not await self._actions.run_else(): _LOGGER.error("Unable to unlock device") diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 466a7334775..d9751fd707b 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,6 +1,8 @@ """Support for ISY994 sensors.""" from __future__ import annotations +from typing import Any, cast + from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity @@ -29,24 +31,24 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 sensor platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] for node in hass_isy_data[ISY994_NODES][SENSOR]: _LOGGER.debug("Loading %s", node.name) - devices.append(ISYSensorEntity(node)) + entities.append(ISYSensorEntity(node)) for vname, vobj in hass_isy_data[ISY994_VARIABLES]: - devices.append(ISYSensorVariableEntity(vname, vobj)) + entities.append(ISYSensorVariableEntity(vname, vobj)) - await migrate_old_unique_ids(hass, SENSOR, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, SENSOR, entities) + async_add_entities(entities) class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" @property - def raw_unit_of_measurement(self) -> dict | str: + def raw_unit_of_measurement(self) -> dict | str | None: """Get the raw unit of measurement for the ISY994 sensor device.""" uom = self._node.uom @@ -59,12 +61,13 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return isy_states if uom in (UOM_ON_OFF, UOM_INDEX): + assert isinstance(uom, str) return uom return UOM_FRIENDLY_NAME.get(uom) @property - def native_value(self) -> str: + def native_value(self) -> float | int | str | None: """Get the state of the ISY994 sensor device.""" if (value := self._node.status) == ISY_VALUE_UNKNOWN: return None @@ -77,11 +80,11 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return uom.get(value, value) if uom in (UOM_INDEX, UOM_ON_OFF): - return self._node.formatted + return cast(str, self._node.formatted) # Check if this is an index type and get formatted value if uom == UOM_INDEX and hasattr(self._node, "formatted"): - return self._node.formatted + return cast(str, self._node.formatted) # Handle ISY precision and rounding value = convert_isy_value_to_hass(value, uom, self._node.prec) @@ -90,10 +93,14 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT): value = self.hass.config.units.temperature(value, uom) + if value is None: + return None + + assert isinstance(value, (int, float)) return value @property - def native_unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str | None: """Get the Home Assistant unit of measurement for the device.""" raw_units = self.raw_unit_of_measurement # Check if this is a known index pair UOM @@ -113,12 +120,12 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): self._name = vname @property - def native_value(self): + def native_value(self) -> float | int | None: """Return the state of the variable.""" return convert_isy_value_to_hass(self._node.status, "", self._node.prec) @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Get the state attributes for the device.""" return { "init_value": convert_isy_value_to_hass( @@ -128,6 +135,6 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity): } @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return "mdi:counter" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index a1dff594a1f..8323394803f 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -1,4 +1,5 @@ """ISY Services and Commands.""" +from __future__ import annotations from typing import Any @@ -93,6 +94,7 @@ def valid_isy_commands(value: Any) -> str: """Validate the command is valid.""" value = str(value).upper() if value in COMMAND_FRIENDLY_NAME: + assert isinstance(value, str) return value raise vol.Invalid("Invalid ISY Command.") @@ -173,7 +175,7 @@ SERVICE_RUN_NETWORK_RESOURCE_SCHEMA = vol.All( @callback -def async_setup_services(hass: HomeAssistant): # noqa: C901 +def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Create and register services for the ISY integration.""" existing_services = hass.services.async_services().get(DOMAIN) if existing_services and any( @@ -234,7 +236,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 """Handle a send program command service call.""" address = service.data.get(CONF_ADDRESS) name = service.data.get(CONF_NAME) - command = service.data.get(CONF_COMMAND) + command = service.data[CONF_COMMAND] isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: @@ -432,7 +434,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901 @callback -def async_unload_services(hass: HomeAssistant): +def async_unload_services(hass: HomeAssistant) -> None: """Unload services for the ISY integration.""" if hass.data[DOMAIN]: # There is still another config entry for this domain, don't remove services. @@ -456,7 +458,7 @@ def async_unload_services(hass: HomeAssistant): @callback -def async_setup_light_services(hass: HomeAssistant): +def async_setup_light_services(hass: HomeAssistant) -> None: """Create device-specific services for the ISY Integration.""" platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 3e72dd6f0ec..a92be5d4d23 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,4 +1,7 @@ """Support for ISY994 switches.""" +from __future__ import annotations + +from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP @@ -17,39 +20,39 @@ async def async_setup_entry( ) -> None: """Set up the ISY994 switch platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - devices = [] + entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] for node in hass_isy_data[ISY994_NODES][SWITCH]: - devices.append(ISYSwitchEntity(node)) + entities.append(ISYSwitchEntity(node)) for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]: - devices.append(ISYSwitchProgramEntity(name, status, actions)) + entities.append(ISYSwitchProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, SWITCH, devices) - async_add_entities(devices) + await migrate_old_unique_ids(hass, SWITCH, entities) + async_add_entities(entities) class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): """Representation of an ISY994 switch device.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Get whether the ISY994 device is in the on state.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 switch.""" if not await self._node.turn_off(): _LOGGER.debug("Unable to turn off switch") - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY994 switch.""" if not await self._node.turn_on(): _LOGGER.debug("Unable to turn on switch") @property - def icon(self) -> str: + def icon(self) -> str | None: """Get the icon for groups.""" if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: return "mdi:google-circles-communities" # Matches isy scene icon @@ -64,12 +67,12 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): """Get whether the ISY994 switch program is on.""" return bool(self._node.status) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Send the turn on command to the ISY994 switch program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn on switch") - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Send the turn off command to the ISY994 switch program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to turn off switch") diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index f550b8ed07b..a8497ba0b27 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -1,7 +1,12 @@ """Provide info to system health.""" +from __future__ import annotations + +from typing import Any + from pyisy import ISY from homeassistant.components import system_health +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback @@ -16,7 +21,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" health_info = {} @@ -26,6 +31,7 @@ async def system_health_info(hass): isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] entry = hass.config_entries.async_get_entry(config_entry_id) + assert isinstance(entry, ConfigEntry) health_info["host_reachable"] = await system_health.async_check_can_reach_url( hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}" ) diff --git a/mypy.ini b/mypy.ini index c48d0221033..cc5d9b0e5e4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -862,6 +862,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.isy994.*] +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.iqvia.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2275,45 +2286,6 @@ ignore_errors = true [mypy-homeassistant.components.input_datetime] ignore_errors = true -[mypy-homeassistant.components.isy994] -ignore_errors = true - -[mypy-homeassistant.components.isy994.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.isy994.climate] -ignore_errors = true - -[mypy-homeassistant.components.isy994.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.isy994.cover] -ignore_errors = true - -[mypy-homeassistant.components.isy994.entity] -ignore_errors = true - -[mypy-homeassistant.components.isy994.fan] -ignore_errors = true - -[mypy-homeassistant.components.isy994.helpers] -ignore_errors = true - -[mypy-homeassistant.components.isy994.light] -ignore_errors = true - -[mypy-homeassistant.components.isy994.lock] -ignore_errors = true - -[mypy-homeassistant.components.isy994.sensor] -ignore_errors = true - -[mypy-homeassistant.components.isy994.services] -ignore_errors = true - -[mypy-homeassistant.components.isy994.switch] -ignore_errors = true - [mypy-homeassistant.components.izone.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6697adfa1d1..6b14803b499 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -101,19 +101,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.sensor", "homeassistant.components.influxdb", "homeassistant.components.input_datetime", - "homeassistant.components.isy994", - "homeassistant.components.isy994.binary_sensor", - "homeassistant.components.isy994.climate", - "homeassistant.components.isy994.config_flow", - "homeassistant.components.isy994.cover", - "homeassistant.components.isy994.entity", - "homeassistant.components.isy994.fan", - "homeassistant.components.isy994.helpers", - "homeassistant.components.isy994.light", - "homeassistant.components.isy994.lock", - "homeassistant.components.isy994.sensor", - "homeassistant.components.isy994.services", - "homeassistant.components.isy994.switch", "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow",