Enable strict typing for isy994 (#65439)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
J. Nick Koston 2022-02-03 10:02:05 -06:00 committed by GitHub
parent 2f0d0998a2
commit 6c38a6b569
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 351 additions and 279 deletions

View File

@ -95,6 +95,7 @@ homeassistant.components.image_processing.*
homeassistant.components.input_button.* homeassistant.components.input_button.*
homeassistant.components.input_select.* homeassistant.components.input_select.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.isy994.*
homeassistant.components.iqvia.* homeassistant.components.iqvia.*
homeassistant.components.jellyfin.* homeassistant.components.jellyfin.*
homeassistant.components.jewish_calendar.* homeassistant.components.jewish_calendar.*

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.device_registry as dr import homeassistant.helpers.device_registry as dr
@ -98,10 +98,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@callback @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): for entry in hass.config_entries.async_entries(DOMAIN):
if entry.source == config_entries.SOURCE_IMPORT: if entry.source == config_entries.SOURCE_IMPORT:
return entry return entry
return None
async def async_setup_entry( async def async_setup_entry(
@ -147,7 +150,7 @@ async def async_setup_entry(
https = False https = False
port = host.port or 80 port = host.port or 80
session = aiohttp_client.async_create_clientsession( 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": elif host.scheme == "https":
https = True https = True
@ -206,7 +209,7 @@ async def async_setup_entry(
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
@callback @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.""" """Stop the isy auto update on Home Assistant Shutdown."""
_LOGGER.debug("ISY Stopping Event Stream and automatic updates") _LOGGER.debug("ISY Stopping Event Stream and automatic updates")
isy.websocket.stop() isy.websocket.stop()
@ -235,7 +238,7 @@ async def _async_update_listener(
@callback @callback
def _async_import_options_from_data_if_missing( def _async_import_options_from_data_if_missing(
hass: HomeAssistant, entry: config_entries.ConfigEntry hass: HomeAssistant, entry: config_entries.ConfigEntry
): ) -> None:
options = dict(entry.options) options = dict(entry.options)
modified = False modified = False
for importable_option in ( for importable_option in (
@ -261,7 +264,7 @@ def _async_isy_to_configuration_url(isy: ISY) -> str:
@callback @callback
def _async_get_or_create_isy_device_in_registry( 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: ) -> None:
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
url = _async_isy_to_configuration_url(isy) url = _async_isy_to_configuration_url(isy)

View File

@ -1,7 +1,8 @@
"""Support for ISY994 binary sensors.""" """Support for ISY994 binary sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any
from pyisy.constants import ( from pyisy.constants import (
CMD_OFF, CMD_OFF,
@ -10,6 +11,7 @@ from pyisy.constants import (
PROTO_INSTEON, PROTO_INSTEON,
PROTO_ZWAVE, PROTO_ZWAVE,
) )
from pyisy.helpers import NodeProperty
from pyisy.nodes import Group, Node from pyisy.nodes import Group, Node
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -18,7 +20,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util 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 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 binary sensor platform.""" """Set up the ISY994 binary sensor platform."""
devices = [] entities: list[
devices_by_address = {} ISYInsteonBinarySensorEntity
child_nodes = [] | 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] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]: for node in hass_isy_data[ISY994_NODES][BINARY_SENSOR]:
assert isinstance(node, Node)
device_class, device_type = _detect_device_type_and_class(node) device_class, device_type = _detect_device_type_and_class(node)
if node.protocol == PROTO_INSTEON: if node.protocol == PROTO_INSTEON:
if node.parent_node is not None: if node.parent_node is not None:
@ -68,38 +83,38 @@ async def async_setup_entry(
# nodes have been processed # nodes have been processed
child_nodes.append((node, device_class, device_type)) child_nodes.append((node, device_class, device_type))
continue continue
device = ISYInsteonBinarySensorEntity(node, device_class) entity = ISYInsteonBinarySensorEntity(node, device_class)
else: else:
device = ISYBinarySensorEntity(node, device_class) entity = ISYBinarySensorEntity(node, device_class)
devices.append(device) entities.append(entity)
devices_by_address[node.address] = device entities_by_address[node.address] = entity
# Handle some special child node cases for Insteon Devices # Handle some special child node cases for Insteon Devices
for (node, device_class, device_type) in child_nodes: for (node, device_class, device_type) in child_nodes:
subnode_id = int(node.address.split(" ")[-1], 16) subnode_id = int(node.address.split(" ")[-1], 16)
# Handle Insteon Thermostats # 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: if subnode_id == SUBNODE_CLIMATE_COOL:
# Subnode 2 is the "Cool Control" sensor # Subnode 2 is the "Cool Control" sensor
# It never reports its state until first use is # It never reports its state until first use is
# detected after an ISY Restart, so we assume it's off. # detected after an ISY Restart, so we assume it's off.
# As soon as the ISY Event Stream connects if it has a # As soon as the ISY Event Stream connects if it has a
# valid state, it will be set. # valid state, it will be set.
device = ISYInsteonBinarySensorEntity( entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.COLD, False node, BinarySensorDeviceClass.COLD, False
) )
devices.append(device) entities.append(entity)
elif subnode_id == SUBNODE_CLIMATE_HEAT: elif subnode_id == SUBNODE_CLIMATE_HEAT:
# Subnode 3 is the "Heat Control" sensor # Subnode 3 is the "Heat Control" sensor
device = ISYInsteonBinarySensorEntity( entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.HEAT, False node, BinarySensorDeviceClass.HEAT, False
) )
devices.append(device) entities.append(entity)
continue continue
if device_class in DEVICE_PARENT_REQUIRED: if device_class in DEVICE_PARENT_REQUIRED:
parent_device = devices_by_address.get(node.parent_node.address) parent_entity = entities_by_address.get(node.parent_node.address)
if not parent_device: if not parent_entity:
_LOGGER.error( _LOGGER.error(
"Node %s has a parent node %s, but no device " "Node %s has a parent node %s, but no device "
"was created for the parent. Skipping", "was created for the parent. Skipping",
@ -115,13 +130,15 @@ async def async_setup_entry(
# These sensors use an optional "negative" subnode 2 to # These sensors use an optional "negative" subnode 2 to
# snag all state changes # snag all state changes
if subnode_id == SUBNODE_NEGATIVE: 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: elif subnode_id == SUBNODE_HEARTBEAT:
assert isinstance(parent_entity, ISYInsteonBinarySensorEntity)
# Subnode 4 is the heartbeat node, which we will # Subnode 4 is the heartbeat node, which we will
# represent as a separate binary_sensor # represent as a separate binary_sensor
device = ISYBinarySensorHeartbeat(node, parent_device) entity = ISYBinarySensorHeartbeat(node, parent_entity)
parent_device.add_heartbeat_device(device) parent_entity.add_heartbeat_device(entity)
devices.append(device) entities.append(entity)
continue continue
if ( if (
device_class == BinarySensorDeviceClass.MOTION device_class == BinarySensorDeviceClass.MOTION
@ -133,48 +150,49 @@ async def async_setup_entry(
# the initial state is forced "OFF"/"NORMAL" if the # the initial state is forced "OFF"/"NORMAL" if the
# parent device has a valid state. This is corrected # parent device has a valid state. This is corrected
# upon connection to the ISY event stream if subnode has a valid state. # 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: if subnode_id == SUBNODE_DUSK_DAWN:
# Subnode 2 is the Dusk/Dawn sensor # Subnode 2 is the Dusk/Dawn sensor
device = ISYInsteonBinarySensorEntity( entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.LIGHT node, BinarySensorDeviceClass.LIGHT
) )
devices.append(device) entities.append(entity)
continue continue
if subnode_id == SUBNODE_LOW_BATTERY: if subnode_id == SUBNODE_LOW_BATTERY:
# Subnode 3 is the low battery node # Subnode 3 is the low battery node
device = ISYInsteonBinarySensorEntity( entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.BATTERY, initial_state node, BinarySensorDeviceClass.BATTERY, initial_state
) )
devices.append(device) entities.append(entity)
continue continue
if subnode_id in SUBNODE_TAMPER: if subnode_id in SUBNODE_TAMPER:
# Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes
# reported as "10", which translate from Hex to 10 and 16 resp. # reported as "10", which translate from Hex to 10 and 16 resp.
device = ISYInsteonBinarySensorEntity( entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.PROBLEM, initial_state node, BinarySensorDeviceClass.PROBLEM, initial_state
) )
devices.append(device) entities.append(entity)
continue continue
if subnode_id in SUBNODE_MOTION_DISABLED: if subnode_id in SUBNODE_MOTION_DISABLED:
# Motion Disabled Sub-node for MS II ("D" or "13") # Motion Disabled Sub-node for MS II ("D" or "13")
device = ISYInsteonBinarySensorEntity(node) entity = ISYInsteonBinarySensorEntity(node)
devices.append(device) entities.append(entity)
continue continue
# We don't yet have any special logic for other sensor # We don't yet have any special logic for other sensor
# types, so add the nodes as individual devices # types, so add the nodes as individual devices
device = ISYBinarySensorEntity(node, device_class) entity = ISYBinarySensorEntity(node, device_class)
devices.append(device) entities.append(entity)
for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: 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) await migrate_old_unique_ids(hass, BINARY_SENSOR, entities)
async_add_entities(devices) 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: try:
device_type = node.type device_type = node.type
except AttributeError: except AttributeError:
@ -199,20 +217,25 @@ def _detect_device_type_and_class(node: Group | Node) -> (str, str):
class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
"""Representation of a basic ISY994 binary sensor device.""" """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.""" """Initialize the ISY994 binary sensor device."""
super().__init__(node) super().__init__(node)
self._device_class = force_device_class self._device_class = force_device_class
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Get whether the ISY994 binary sensor device is on.""" """Get whether the ISY994 binary sensor device is on."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return bool(self._node.status) return bool(self._node.status)
@property @property
def device_class(self) -> str: def device_class(self) -> str | None:
"""Return the class of this device. """Return the class of this device.
This was discovered by parsing the device type code during init 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. 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.""" """Initialize the ISY994 binary sensor device."""
super().__init__(node, force_device_class) super().__init__(node, force_device_class)
self._negative_node = None self._negative_node: Node | None = None
self._heartbeat_device = None self._heartbeat_device: ISYBinarySensorHeartbeat | None = None
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
self._computed_state = unknown_state self._computed_state = unknown_state
self._status_was_unknown = True self._status_was_unknown = True
@ -252,21 +280,21 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
self._async_negative_node_control_handler 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. """Register a heartbeat device for this sensor.
The heartbeat node beats on its own, but we can gain a little The heartbeat node beats on its own, but we can gain a little
reliability by considering any node activity for this sensor reliability by considering any node activity for this sensor
to be a heartbeat as well. to be a heartbeat as well.
""" """
self._heartbeat_device = device self._heartbeat_device = entity
def _async_heartbeat(self) -> None: def _async_heartbeat(self) -> None:
"""Send a heartbeat to our heartbeat device, if we have one.""" """Send a heartbeat to our heartbeat device, if we have one."""
if self._heartbeat_device is not None: if self._heartbeat_device is not None:
self._heartbeat_device.async_heartbeat() 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. """Add a negative node to this binary sensor device.
The negative node is a node that can receive the 'off' events The negative node is a node that can receive the 'off' events
@ -287,7 +315,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
self._computed_state = None self._computed_state = None
@callback @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.""" """Handle an "On" control event from the "negative" node."""
if event.control == CMD_ON: if event.control == CMD_ON:
_LOGGER.debug( _LOGGER.debug(
@ -299,7 +327,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
self._async_heartbeat() self._async_heartbeat()
@callback @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. """Handle On and Off control event coming from the primary node.
Depending on device configuration, sometimes only On events Depending on device configuration, sometimes only On events
@ -324,7 +352,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
self._async_heartbeat() self._async_heartbeat()
@callback @callback
def async_on_update(self, event: object) -> None: def async_on_update(self, event: NodeProperty) -> None:
"""Primary node status updates. """Primary node status updates.
We MOSTLY ignore these updates, as we listen directly to the Control We MOSTLY ignore these updates, as we listen directly to the Control
@ -341,7 +369,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
self._async_heartbeat() self._async_heartbeat()
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Get whether the ISY994 binary sensor device is on. """Get whether the ISY994 binary sensor device is on.
Insteon leak sensors set their primary node to On when the state is Insteon leak sensors set their primary node to On when the state is
@ -361,7 +389,14 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
"""Representation of the battery state of an ISY994 sensor.""" """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. """Initialize the ISY994 binary sensor device.
Computed state is set to UNKNOWN unless the ISY provided a valid Computed state is set to UNKNOWN unless the ISY provided a valid
@ -372,8 +407,8 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
""" """
super().__init__(node) super().__init__(node)
self._parent_device = parent_device self._parent_device = parent_device
self._heartbeat_timer = None self._heartbeat_timer: CALLBACK_TYPE | None = None
self._computed_state = None self._computed_state: bool | None = None
if self.state is None: if self.state is None:
self._computed_state = False 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 # Start the timer on bootup, so we can change from UNKNOWN to OFF
self._restart_timer() 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. """Update the heartbeat timestamp when any ON/OFF event is sent.
The ISY uses both DON and DOF commands (alternating) for a heartbeat. The ISY uses both DON and DOF commands (alternating) for a heartbeat.
@ -395,7 +430,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
self.async_heartbeat() self.async_heartbeat()
@callback @callback
def async_heartbeat(self): def async_heartbeat(self) -> None:
"""Mark the device as online, and restart the 25 hour timer. """Mark the device as online, and restart the 25 hour timer.
This gets called when the heartbeat node beats, but also when the This gets called when the heartbeat node beats, but also when the
@ -407,17 +442,14 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
self._restart_timer() self._restart_timer()
self.async_write_ha_state() self.async_write_ha_state()
def _restart_timer(self): def _restart_timer(self) -> None:
"""Restart the 25 hour timer.""" """Restart the 25 hour timer."""
try: if self._heartbeat_timer is not None:
self._heartbeat_timer() self._heartbeat_timer()
self._heartbeat_timer = None self._heartbeat_timer = None
except TypeError:
# No heartbeat timer is active
pass
@callback @callback
def timer_elapsed(now) -> None: def timer_elapsed(now: datetime) -> None:
"""Heartbeat missed; set state to ON to indicate dead battery.""" """Heartbeat missed; set state to ON to indicate dead battery."""
self._computed_state = True self._computed_state = True
self._heartbeat_timer = None self._heartbeat_timer = None
@ -457,7 +489,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
return BinarySensorDeviceClass.BATTERY return BinarySensorDeviceClass.BATTERY
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Get the state attributes for the device.""" """Get the state attributes for the device."""
attr = super().extra_state_attributes attr = super().extra_state_attributes
attr["parent_entity_id"] = self._parent_device.entity_id attr["parent_entity_id"] = self._parent_device.entity_id

View File

@ -1,6 +1,8 @@
"""Support for Insteon Thermostats via ISY994 Platform.""" """Support for Insteon Thermostats via ISY994 Platform."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from pyisy.constants import ( from pyisy.constants import (
CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_FAN_SETTING,
CMD_CLIMATE_MODE, CMD_CLIMATE_MODE,
@ -11,6 +13,7 @@ from pyisy.constants import (
PROP_UOM, PROP_UOM,
PROTO_INSTEON, PROTO_INSTEON,
) )
from pyisy.nodes import Node
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -18,9 +21,11 @@ from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE, DOMAIN as CLIMATE,
FAN_AUTO, FAN_AUTO,
FAN_OFF,
FAN_ON, FAN_ON,
HVAC_MODE_COOL, HVAC_MODE_COOL,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_FAN_MODE, SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE_RANGE,
@ -76,16 +81,15 @@ async def async_setup_entry(
class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
"""Representation of an ISY994 thermostat entity.""" """Representation of an ISY994 thermostat entity."""
def __init__(self, node) -> None: def __init__(self, node: Node) -> None:
"""Initialize the ISY Thermostat entity.""" """Initialize the ISY Thermostat entity."""
super().__init__(node) super().__init__(node)
self._node = node
self._uom = self._node.uom self._uom = self._node.uom
if isinstance(self._uom, list): if isinstance(self._uom, list):
self._uom = self._node.uom[0] self._uom = self._node.uom[0]
self._hvac_action = None self._hvac_action: str | None = None
self._hvac_mode = None self._hvac_mode: str | None = None
self._fan_mode = None self._fan_mode: str | None = None
self._temp_unit = None self._temp_unit = None
self._current_humidity = 0 self._current_humidity = 0
self._target_temp_low = 0 self._target_temp_low = 0
@ -97,7 +101,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
return ISY_SUPPORTED_FEATURES return ISY_SUPPORTED_FEATURES
@property @property
def precision(self) -> str: def precision(self) -> float:
"""Return the precision of the system.""" """Return the precision of the system."""
return PRECISION_TENTHS return PRECISION_TENTHS
@ -110,6 +114,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
return TEMP_CELSIUS return TEMP_CELSIUS
if uom.value == UOM_ISY_FAHRENHEIT: if uom.value == UOM_ISY_FAHRENHEIT:
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
return TEMP_FAHRENHEIT
@property @property
def current_humidity(self) -> int | None: def current_humidity(self) -> int | None:
@ -119,10 +124,10 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
return int(humidity.value) return int(humidity.value)
@property @property
def hvac_mode(self) -> str | None: def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.""" """Return hvac operation ie. heat, cool mode."""
if not (hvac_mode := self._node.aux_properties.get(CMD_CLIMATE_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: # Which state values used depends on the mode property's UOM:
uom = hvac_mode.uom uom = hvac_mode.uom
@ -133,7 +138,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
if self._node.protocol == PROTO_INSTEON if self._node.protocol == PROTO_INSTEON
else UOM_HVAC_MODE_GENERIC 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 @property
def hvac_modes(self) -> list[str]: 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) return convert_isy_value_to_hass(target.value, target.uom, target.prec, 1)
@property @property
def fan_modes(self): def fan_modes(self) -> list[str]:
"""Return the list of available fan modes.""" """Return the list of available fan modes."""
return [FAN_AUTO, FAN_ON] return [FAN_AUTO, FAN_ON]
@ -195,10 +200,10 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
"""Return the current fan mode ie. auto, on.""" """Return the current fan mode ie. auto, on."""
fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING) fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING)
if not fan_mode: if not fan_mode:
return None return FAN_OFF
return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value) 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.""" """Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp = kwargs.get(ATTR_TEMPERATURE)
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)

View File

@ -1,5 +1,8 @@
"""Config flow for Universal Devices ISY994 integration.""" """Config flow for Universal Devices ISY994 integration."""
from __future__ import annotations
import logging import logging
from typing import Any
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
from aiohttp import CookieJar from aiohttp import CookieJar
@ -38,7 +41,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _data_schema(schema_input): def _data_schema(schema_input: dict[str, str]) -> vol.Schema:
"""Generate schema with defaults.""" """Generate schema with defaults."""
return vol.Schema( 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. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user. 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 https = False
port = host.port or HTTP_PORT port = host.port or HTTP_PORT
session = aiohttp_client.async_create_clientsession( 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: elif host.scheme == SCHEME_HTTPS:
https = True https = True
@ -113,18 +118,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the isy994 config flow.""" """Initialize the isy994 config flow."""
self.discovered_conf = {} self.discovered_conf: dict[str, str] = {}
@staticmethod @staticmethod
@callback @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.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) 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.""" """Handle the initial step."""
errors = {} errors = {}
info = None info: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: try:
info = await validate_input(self.hass, user_input) info = await validate_input(self.hass, user_input)
@ -149,11 +158,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, 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.""" """Handle import."""
return await self.async_step_user(user_input) 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.""" """Abort and update the ip address on change."""
existing_entry = await self.async_set_unique_id(isy_mac) existing_entry = await self.async_set_unique_id(isy_mac)
if not existing_entry: if not existing_entry:
@ -211,6 +224,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a discovered isy994.""" """Handle a discovered isy994."""
friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
url = discovery_info.ssdp_location url = discovery_info.ssdp_location
assert isinstance(url, str)
parsed_url = urlparse(url) parsed_url = urlparse(url)
mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN] mac = discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
if mac.startswith(UDN_UUID_PREFIX): if mac.startswith(UDN_UUID_PREFIX):
@ -224,6 +238,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
elif parsed_url.scheme == SCHEME_HTTPS: elif parsed_url.scheme == SCHEME_HTTPS:
port = HTTPS_PORT port = HTTPS_PORT
assert isinstance(parsed_url.hostname, str)
await self._async_set_unique_id_or_update(mac, parsed_url.hostname, port) await self._async_set_unique_id_or_update(mac, parsed_url.hostname, port)
self.discovered_conf = { self.discovered_conf = {
@ -242,7 +257,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry 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.""" """Handle options flow."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View File

@ -204,7 +204,7 @@ UOM_PERCENTAGE = "51"
# responses, not using them for Home Assistant states # responses, not using them for Home Assistant states
# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml # 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 # 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: { Platform.BINARY_SENSOR: {
FILTER_UOM: [UOM_ON_OFF], FILTER_UOM: [UOM_ON_OFF],
FILTER_STATES: [], FILTER_STATES: [],

View File

@ -1,4 +1,7 @@
"""Support for ISY994 covers.""" """Support for ISY994 covers."""
from __future__ import annotations
from typing import Any
from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.constants import ISY_VALUE_UNKNOWN
@ -31,53 +34,53 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the ISY994 cover platform.""" """Set up the ISY994 cover platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
devices = [] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][COVER]: 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]: 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) await migrate_old_unique_ids(hass, COVER, entities)
async_add_entities(devices) async_add_entities(entities)
class ISYCoverEntity(ISYNodeEntity, CoverEntity): class ISYCoverEntity(ISYNodeEntity, CoverEntity):
"""Representation of an ISY994 cover device.""" """Representation of an ISY994 cover device."""
@property @property
def current_cover_position(self) -> int: def current_cover_position(self) -> int | None:
"""Return the current cover position.""" """Return the current cover position."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
if self._node.uom == UOM_8_BIT_RANGE: if self._node.uom == UOM_8_BIT_RANGE:
return round(self._node.status * 100.0 / 255.0) 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 @property
def is_closed(self) -> bool: def is_closed(self) -> bool | None:
"""Get whether the ISY994 cover device is closed.""" """Get whether the ISY994 cover device is closed."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return self._node.status == 0 return bool(self._node.status == 0)
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION 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.""" """Send the open cover command to the ISY994 cover device."""
val = 100 if self._node.uom == UOM_BARRIER else None val = 100 if self._node.uom == UOM_BARRIER else None
if not await self._node.turn_on(val=val): if not await self._node.turn_on(val=val):
_LOGGER.error("Unable to open the cover") _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.""" """Send the close cover command to the ISY994 cover device."""
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.error("Unable to close the cover") _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.""" """Move the cover to a specific position."""
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
if self._node.uom == UOM_8_BIT_RANGE: if self._node.uom == UOM_8_BIT_RANGE:
@ -94,12 +97,12 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
"""Get whether the ISY994 cover program is closed.""" """Get whether the ISY994 cover program is closed."""
return bool(self._node.status) 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.""" """Send the open cover command to the ISY994 cover program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to open the cover") _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.""" """Send the close cover command to the ISY994 cover program."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to close the cover") _LOGGER.error("Unable to close the cover")

View File

@ -1,6 +1,8 @@
"""Representation of ISYEntity Types.""" """Representation of ISYEntity Types."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast
from pyisy.constants import ( from pyisy.constants import (
COMMAND_FRIENDLY_NAME, COMMAND_FRIENDLY_NAME,
EMPTY_TIME, EMPTY_TIME,
@ -8,7 +10,9 @@ from pyisy.constants import (
PROTO_GROUP, PROTO_GROUP,
PROTO_ZWAVE, 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 ( from homeassistant.const import (
ATTR_IDENTIFIERS, ATTR_IDENTIFIERS,
@ -30,14 +34,14 @@ from .const import DOMAIN
class ISYEntity(Entity): class ISYEntity(Entity):
"""Representation of an ISY994 device.""" """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.""" """Initialize the insteon device."""
self._node = node self._node = node
self._attrs = {} self._attrs: dict[str, Any] = {}
self._change_handler = None self._change_handler: EventListener | None = None
self._control_handler = None self._control_handler: EventListener | None = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to the node change events.""" """Subscribe to the node change events."""
@ -49,7 +53,7 @@ class ISYEntity(Entity):
) )
@callback @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.""" """Handle the update event from the ISY994 Node."""
self.async_write_ha_state() self.async_write_ha_state()
@ -72,7 +76,7 @@ class ISYEntity(Entity):
self.hass.bus.fire("isy994_control", event_data) self.hass.bus.fire("isy994_control", event_data)
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo | None:
"""Return the device_info of the device.""" """Return the device_info of the device."""
if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP:
# not a device # not a device
@ -90,7 +94,6 @@ class ISYEntity(Entity):
basename = node.name basename = node.name
device_info = DeviceInfo( device_info = DeviceInfo(
identifiers={},
manufacturer="Unknown", manufacturer="Unknown",
model="Unknown", model="Unknown",
name=basename, name=basename,
@ -99,25 +102,30 @@ class ISYEntity(Entity):
) )
if hasattr(node, "address"): 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"): if hasattr(node, "primary_node"):
device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")}
# ISYv5 Device Types # ISYv5 Device Types
if hasattr(node, "node_def_id") and node.node_def_id is not None: 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 # Numerical Device Type
if hasattr(node, "type") and node.type is not None: 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"): 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: if node.protocol == PROTO_ZWAVE:
# Get extra information for Z-Wave Devices # Get extra information for Z-Wave Devices
device_info[ATTR_MANUFACTURER] += f" MfrID:{node.zwave_props.mfr_id}" manufacturer += f" MfrID:{node.zwave_props.mfr_id}"
device_info[ATTR_MODEL] += ( model += (
f" Type:{node.zwave_props.devtype_gen} " f" Type:{node.zwave_props.devtype_gen} "
f"ProductTypeID:{node.zwave_props.prod_type_id} " f"ProductTypeID:{node.zwave_props.prod_type_id} "
f"ProductID:{node.zwave_props.product_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: if hasattr(node, "folder") and node.folder is not None:
device_info[ATTR_SUGGESTED_AREA] = node.folder device_info[ATTR_SUGGESTED_AREA] = node.folder
# Note: sw_version is not exposed by the ISY for the individual devices. # Note: sw_version is not exposed by the ISY for the individual devices.
@ -125,17 +133,17 @@ class ISYEntity(Entity):
return device_info return device_info
@property @property
def unique_id(self) -> str: def unique_id(self) -> str | None:
"""Get the unique identifier of the device.""" """Get the unique identifier of the device."""
if hasattr(self._node, "address"): if hasattr(self._node, "address"):
return f"{self._node.isy.configuration['uuid']}_{self._node.address}" return f"{self._node.isy.configuration['uuid']}_{self._node.address}"
return None return None
@property @property
def old_unique_id(self) -> str: def old_unique_id(self) -> str | None:
"""Get the old unique identifier of the device.""" """Get the old unique identifier of the device."""
if hasattr(self._node, "address"): if hasattr(self._node, "address"):
return self._node.address return cast(str, self._node.address)
return None return None
@property @property
@ -174,7 +182,7 @@ class ISYNodeEntity(ISYEntity):
self._attrs.update(attr) self._attrs.update(attr)
return self._attrs 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.""" """Respond to an entity service command call."""
if not hasattr(self._node, command): if not hasattr(self._node, command):
raise HomeAssistantError( raise HomeAssistantError(
@ -183,8 +191,12 @@ class ISYNodeEntity(ISYEntity):
await getattr(self._node, command)() await getattr(self._node, command)()
async def async_send_raw_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.""" """Respond to an entity service raw command call."""
if not hasattr(self._node, "send_cmd"): if not hasattr(self._node, "send_cmd"):
raise HomeAssistantError( raise HomeAssistantError(
@ -192,7 +204,7 @@ class ISYNodeEntity(ISYEntity):
) )
await self._node.send_cmd(command, value, unit_of_measurement, parameters) 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.""" """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: if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError( raise HomeAssistantError(
@ -200,7 +212,9 @@ class ISYNodeEntity(ISYEntity):
) )
await self._node.get_zwave_parameter(parameter) 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.""" """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: if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError( raise HomeAssistantError(
@ -209,7 +223,7 @@ class ISYNodeEntity(ISYEntity):
await self._node.set_zwave_parameter(parameter, value, size) await self._node.set_zwave_parameter(parameter, value, size)
await self._node.get_zwave_parameter(parameter) 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.""" """Respond to an entity service command to rename a node on the ISY."""
await self._node.rename(name) await self._node.rename(name)
@ -217,7 +231,7 @@ class ISYNodeEntity(ISYEntity):
class ISYProgramEntity(ISYEntity): class ISYProgramEntity(ISYEntity):
"""Representation of an ISY994 program base.""" """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.""" """Initialize the ISY994 program-based entity."""
super().__init__(status) super().__init__(status)
self._name = name self._name = name

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import math import math
from typing import Any
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON
@ -27,16 +28,16 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the ISY994 fan platform.""" """Set up the ISY994 fan platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
devices = [] entities: list[ISYFanEntity | ISYFanProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][FAN]: 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]: 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) await migrate_old_unique_ids(hass, FAN, entities)
async_add_entities(devices) async_add_entities(entities)
class ISYFanEntity(ISYNodeEntity, FanEntity): class ISYFanEntity(ISYNodeEntity, FanEntity):
@ -57,11 +58,11 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
return int_states_in_range(SPEED_RANGE) return int_states_in_range(SPEED_RANGE)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Get if the fan is on.""" """Get if the fan is on."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return self._node.status != 0 return bool(self._node.status != 0)
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set node to speed percentage for the ISY994 fan device.""" """Set node to speed percentage for the ISY994 fan device."""
@ -75,15 +76,15 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
async def async_turn_on( async def async_turn_on(
self, self,
speed: str = None, speed: str | None = None,
percentage: int = None, percentage: int | None = None,
preset_mode: str = None, preset_mode: str | None = None,
**kwargs, **kwargs: Any,
) -> None: ) -> None:
"""Send the turn on command to the ISY994 fan device.""" """Send the turn on command to the ISY994 fan device."""
await self.async_set_percentage(percentage or 67) 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.""" """Send the turn off command to the ISY994 fan device."""
await self._node.turn_off() await self._node.turn_off()
@ -111,19 +112,19 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get if the fan is on.""" """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.""" """Send the turn on command to ISY994 fan program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to turn off the fan") _LOGGER.error("Unable to turn off the fan")
async def async_turn_on( async def async_turn_on(
self, self,
speed: str = None, speed: str | None = None,
percentage: int = None, percentage: int | None = None,
preset_mode: str = None, preset_mode: str | None = None,
**kwargs, **kwargs: Any,
) -> None: ) -> None:
"""Send the turn off command to ISY994 fan program.""" """Send the turn off command to ISY994 fan program."""
if not await self._actions.run_else(): if not await self._actions.run_else():

View File

@ -1,7 +1,8 @@
"""Sorting helpers for ISY994 device classifications.""" """Sorting helpers for ISY994 device classifications."""
from __future__ import annotations from __future__ import annotations
from typing import Any from collections.abc import Sequence
from typing import TYPE_CHECKING, cast
from pyisy.constants import ( from pyisy.constants import (
ISY_VALUE_UNKNOWN, 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.light import DOMAIN as LIGHT
from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.entity_registry import async_get_registry
@ -53,12 +55,15 @@ from .const import (
UOM_ISYV4_DEGREES, UOM_ISYV4_DEGREES,
) )
if TYPE_CHECKING:
from .entity import ISYEntity
BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_UOMS = ["2", "78"]
BINARY_SENSOR_ISY_STATES = ["on", "off"] BINARY_SENSOR_ISY_STATES = ["on", "off"]
def _check_for_node_def( 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: ) -> bool:
"""Check if the node matches the node_def_id for any platforms. """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( 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: ) -> bool:
"""Check if the node matches the Insteon type for any platforms. """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( 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: ) -> bool:
"""Check if the node matches the ISY Z-Wave Category for any platforms. """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( def _check_for_uom_id(
hass_isy_data: dict, hass_isy_data: dict,
node: Group | Node, node: Group | Node,
single_platform: str = None, single_platform: Platform | None = None,
uom_list: list = None, uom_list: list[str] | None = None,
) -> bool: ) -> bool:
"""Check if a node's uom matches any of the platforms uom filter. """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( def _check_for_states_in_uom(
hass_isy_data: dict, hass_isy_data: dict,
node: Group | Node, node: Group | Node,
single_platform: str = None, single_platform: Platform | None = None,
states_list: list = None, states_list: list[str] | None = None,
) -> bool: ) -> bool:
"""Check if a list of uoms matches two possible filters. """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: 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.""" """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 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 return True
# For the next two checks, we're providing our own set of uoms that # 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 # checks in the context of already knowing that this is definitely a
# sensor device. # sensor device.
if _check_for_uom_id( 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 return True
if _check_for_states_in_uom( if _check_for_states_in_uom(
hass_isy_data, hass_isy_data,
node, node,
single_platform=BINARY_SENSOR, single_platform=Platform.BINARY_SENSOR,
states_list=BINARY_SENSOR_ISY_STATES, states_list=BINARY_SENSOR_ISY_STATES,
): ):
return True return True
@ -275,7 +285,7 @@ def _categorize_nodes(
hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
) -> None: ) -> None:
"""Sort the nodes to their proper platforms.""" """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 ignored = ignore_identifier in path or ignore_identifier in node.name
if ignored: if ignored:
# Don't import this node as a device at all # Don't import this node as a device at all
@ -365,43 +375,45 @@ def _categorize_variables(
async def migrate_old_unique_ids( async def migrate_old_unique_ids(
hass: HomeAssistant, platform: str, devices: list[Any] | None hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity]
) -> None: ) -> None:
"""Migrate to new controller-specific unique ids.""" """Migrate to new controller-specific unique ids."""
registry = await async_get_registry(hass) 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( 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: if old_entity_id is not None:
_LOGGER.debug( _LOGGER.debug(
"Migrating unique_id from [%s] to [%s]", "Migrating unique_id from [%s] to [%s]",
device.old_unique_id, entity.old_unique_id,
device.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( 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: if old_entity_id_2 is not None:
_LOGGER.debug( _LOGGER.debug(
"Migrating unique_id from [%s] to [%s]", "Migrating unique_id from [%s] to [%s]",
device.unique_id.replace(":", ""), entity.unique_id.replace(":", ""),
device.unique_id, entity.unique_id,
) )
registry.async_update_entity( 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( def convert_isy_value_to_hass(
value: int | float | None, value: int | float | None,
uom: str, uom: str | None,
precision: int | str, precision: int | str,
fallback_precision: int | None = None, fallback_precision: int | None = None,
) -> float | int: ) -> float | int | None:
"""Fix ISY Reported Values. """Fix ISY Reported Values.
ISY provides float values as an integer and precision component. 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): if uom in (UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES):
return round(float(value) / 2.0, 1) return round(float(value) / 2.0, 1)
if precision not in ("0", 0): 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: if fallback_precision:
return round(float(value), fallback_precision) return round(float(value), fallback_precision)
return value return value

View File

@ -1,7 +1,11 @@
"""Support for ISY994 lights.""" """Support for ISY994 lights."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.constants import ISY_VALUE_UNKNOWN
from pyisy.helpers import NodeProperty
from pyisy.nodes import Node
from homeassistant.components.light import ( from homeassistant.components.light import (
DOMAIN as LIGHT, DOMAIN as LIGHT,
@ -35,22 +39,22 @@ async def async_setup_entry(
isy_options = entry.options isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
devices = [] entities = []
for node in hass_isy_data[ISY994_NODES][LIGHT]: 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) await migrate_old_unique_ids(hass, LIGHT, entities)
async_add_entities(devices) async_add_entities(entities)
async_setup_light_services(hass) async_setup_light_services(hass)
class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
"""Representation of an ISY994 light device.""" """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.""" """Initialize the ISY994 light device."""
super().__init__(node) super().__init__(node)
self._last_brightness = None self._last_brightness: int | None = None
self._restore_light_state = restore_light_state self._restore_light_state = restore_light_state
@property @property
@ -61,7 +65,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
return int(self._node.status) != 0 return int(self._node.status) != 0
@property @property
def brightness(self) -> float: def brightness(self) -> int | None:
"""Get the brightness of the ISY994 light.""" """Get the brightness of the ISY994 light."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
@ -70,14 +74,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
return round(self._node.status * 255.0 / 100.0) return round(self._node.status * 255.0 / 100.0)
return int(self._node.status) 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.""" """Send the turn off command to the ISY994 light device."""
self._last_brightness = self.brightness self._last_brightness = self.brightness
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.debug("Unable to turn off light") _LOGGER.debug("Unable to turn off light")
@callback @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.""" """Save brightness in the update event from the ISY994 Node."""
if self._node.status not in (0, ISY_VALUE_UNKNOWN): if self._node.status not in (0, ISY_VALUE_UNKNOWN):
self._last_brightness = self._node.status self._last_brightness = self._node.status
@ -88,7 +92,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
super().async_on_update(event) super().async_on_update(event)
# pylint: disable=arguments-differ # 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.""" """Send the turn on command to the ISY994 light device."""
if self._restore_light_state and brightness is None and self._last_brightness: if self._restore_light_state and brightness is None and self._last_brightness:
brightness = self._last_brightness brightness = self._last_brightness
@ -99,14 +103,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
_LOGGER.debug("Unable to turn on light") _LOGGER.debug("Unable to turn on light")
@property @property
def extra_state_attributes(self) -> dict: def extra_state_attributes(self) -> dict[str, Any]:
"""Return the light attributes.""" """Return the light attributes."""
attribs = super().extra_state_attributes attribs = super().extra_state_attributes
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
return attribs return attribs
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
@ -124,10 +128,10 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
): ):
self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] 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.""" """Set the ON Level for a device."""
await self._node.set_on_level(value) 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.""" """Set the Ramp Rate for a device."""
await self._node.set_ramp_rate(value) await self._node.set_ramp_rate(value)

View File

@ -1,4 +1,7 @@
"""Support for ISY994 locks.""" """Support for ISY994 locks."""
from __future__ import annotations
from typing import Any
from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.constants import ISY_VALUE_UNKNOWN
@ -19,33 +22,33 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the ISY994 lock platform.""" """Set up the ISY994 lock platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
devices = [] entities: list[ISYLockEntity | ISYLockProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][LOCK]: 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]: 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) await migrate_old_unique_ids(hass, LOCK, entities)
async_add_entities(devices) async_add_entities(entities)
class ISYLockEntity(ISYNodeEntity, LockEntity): class ISYLockEntity(ISYNodeEntity, LockEntity):
"""Representation of an ISY994 lock device.""" """Representation of an ISY994 lock device."""
@property @property
def is_locked(self) -> bool: def is_locked(self) -> bool | None:
"""Get whether the lock is in locked state.""" """Get whether the lock is in locked state."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return VALUE_TO_STATE.get(self._node.status) 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.""" """Send the lock command to the ISY994 device."""
if not await self._node.secure_lock(): if not await self._node.secure_lock():
_LOGGER.error("Unable to lock device") _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.""" """Send the unlock command to the ISY994 device."""
if not await self._node.secure_unlock(): if not await self._node.secure_unlock():
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
@ -59,12 +62,12 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
"""Return true if the device is locked.""" """Return true if the device is locked."""
return bool(self._node.status) return bool(self._node.status)
async def async_lock(self, **kwargs) -> None: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device.""" """Lock the device."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs) -> None: async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device.""" """Unlock the device."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to unlock device") _LOGGER.error("Unable to unlock device")

View File

@ -1,6 +1,8 @@
"""Support for ISY994 sensors.""" """Support for ISY994 sensors."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast
from pyisy.constants import ISY_VALUE_UNKNOWN from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
@ -29,24 +31,24 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the ISY994 sensor platform.""" """Set up the ISY994 sensor platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
devices = [] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
for node in hass_isy_data[ISY994_NODES][SENSOR]: for node in hass_isy_data[ISY994_NODES][SENSOR]:
_LOGGER.debug("Loading %s", node.name) _LOGGER.debug("Loading %s", node.name)
devices.append(ISYSensorEntity(node)) entities.append(ISYSensorEntity(node))
for vname, vobj in hass_isy_data[ISY994_VARIABLES]: 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) await migrate_old_unique_ids(hass, SENSOR, entities)
async_add_entities(devices) async_add_entities(entities)
class ISYSensorEntity(ISYNodeEntity, SensorEntity): class ISYSensorEntity(ISYNodeEntity, SensorEntity):
"""Representation of an ISY994 sensor device.""" """Representation of an ISY994 sensor device."""
@property @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.""" """Get the raw unit of measurement for the ISY994 sensor device."""
uom = self._node.uom uom = self._node.uom
@ -59,12 +61,13 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
return isy_states return isy_states
if uom in (UOM_ON_OFF, UOM_INDEX): if uom in (UOM_ON_OFF, UOM_INDEX):
assert isinstance(uom, str)
return uom return uom
return UOM_FRIENDLY_NAME.get(uom) return UOM_FRIENDLY_NAME.get(uom)
@property @property
def native_value(self) -> str: def native_value(self) -> float | int | str | None:
"""Get the state of the ISY994 sensor device.""" """Get the state of the ISY994 sensor device."""
if (value := self._node.status) == ISY_VALUE_UNKNOWN: if (value := self._node.status) == ISY_VALUE_UNKNOWN:
return None return None
@ -77,11 +80,11 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
return uom.get(value, value) return uom.get(value, value)
if uom in (UOM_INDEX, UOM_ON_OFF): 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 # Check if this is an index type and get formatted value
if uom == UOM_INDEX and hasattr(self._node, "formatted"): if uom == UOM_INDEX and hasattr(self._node, "formatted"):
return self._node.formatted return cast(str, self._node.formatted)
# Handle ISY precision and rounding # Handle ISY precision and rounding
value = convert_isy_value_to_hass(value, uom, self._node.prec) 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): if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
value = self.hass.config.units.temperature(value, uom) value = self.hass.config.units.temperature(value, uom)
if value is None:
return None
assert isinstance(value, (int, float))
return value return value
@property @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.""" """Get the Home Assistant unit of measurement for the device."""
raw_units = self.raw_unit_of_measurement raw_units = self.raw_unit_of_measurement
# Check if this is a known index pair UOM # Check if this is a known index pair UOM
@ -113,12 +120,12 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
self._name = vname self._name = vname
@property @property
def native_value(self): def native_value(self) -> float | int | None:
"""Return the state of the variable.""" """Return the state of the variable."""
return convert_isy_value_to_hass(self._node.status, "", self._node.prec) return convert_isy_value_to_hass(self._node.status, "", self._node.prec)
@property @property
def extra_state_attributes(self) -> dict: def extra_state_attributes(self) -> dict[str, Any]:
"""Get the state attributes for the device.""" """Get the state attributes for the device."""
return { return {
"init_value": convert_isy_value_to_hass( "init_value": convert_isy_value_to_hass(
@ -128,6 +135,6 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
} }
@property @property
def icon(self): def icon(self) -> str:
"""Return the icon.""" """Return the icon."""
return "mdi:counter" return "mdi:counter"

View File

@ -1,4 +1,5 @@
"""ISY Services and Commands.""" """ISY Services and Commands."""
from __future__ import annotations
from typing import Any from typing import Any
@ -93,6 +94,7 @@ def valid_isy_commands(value: Any) -> str:
"""Validate the command is valid.""" """Validate the command is valid."""
value = str(value).upper() value = str(value).upper()
if value in COMMAND_FRIENDLY_NAME: if value in COMMAND_FRIENDLY_NAME:
assert isinstance(value, str)
return value return value
raise vol.Invalid("Invalid ISY Command.") raise vol.Invalid("Invalid ISY Command.")
@ -173,7 +175,7 @@ SERVICE_RUN_NETWORK_RESOURCE_SCHEMA = vol.All(
@callback @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.""" """Create and register services for the ISY integration."""
existing_services = hass.services.async_services().get(DOMAIN) existing_services = hass.services.async_services().get(DOMAIN)
if existing_services and any( if existing_services and any(
@ -234,7 +236,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
"""Handle a send program command service call.""" """Handle a send program command service call."""
address = service.data.get(CONF_ADDRESS) address = service.data.get(CONF_ADDRESS)
name = service.data.get(CONF_NAME) name = service.data.get(CONF_NAME)
command = service.data.get(CONF_COMMAND) command = service.data[CONF_COMMAND]
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
@ -432,7 +434,7 @@ def async_setup_services(hass: HomeAssistant): # noqa: C901
@callback @callback
def async_unload_services(hass: HomeAssistant): def async_unload_services(hass: HomeAssistant) -> None:
"""Unload services for the ISY integration.""" """Unload services for the ISY integration."""
if hass.data[DOMAIN]: if hass.data[DOMAIN]:
# There is still another config entry for this domain, don't remove services. # There is still another config entry for this domain, don't remove services.
@ -456,7 +458,7 @@ def async_unload_services(hass: HomeAssistant):
@callback @callback
def async_setup_light_services(hass: HomeAssistant): def async_setup_light_services(hass: HomeAssistant) -> None:
"""Create device-specific services for the ISY Integration.""" """Create device-specific services for the ISY Integration."""
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()

View File

@ -1,4 +1,7 @@
"""Support for ISY994 switches.""" """Support for ISY994 switches."""
from __future__ import annotations
from typing import Any
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
@ -17,39 +20,39 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the ISY994 switch platform.""" """Set up the ISY994 switch platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
devices = [] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
for node in hass_isy_data[ISY994_NODES][SWITCH]: 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]: 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) await migrate_old_unique_ids(hass, SWITCH, entities)
async_add_entities(devices) async_add_entities(entities)
class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
"""Representation of an ISY994 switch device.""" """Representation of an ISY994 switch device."""
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Get whether the ISY994 device is in the on state.""" """Get whether the ISY994 device is in the on state."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return bool(self._node.status) 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.""" """Send the turn off command to the ISY994 switch."""
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.debug("Unable to turn off switch") _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.""" """Send the turn on command to the ISY994 switch."""
if not await self._node.turn_on(): if not await self._node.turn_on():
_LOGGER.debug("Unable to turn on switch") _LOGGER.debug("Unable to turn on switch")
@property @property
def icon(self) -> str: def icon(self) -> str | None:
"""Get the icon for groups.""" """Get the icon for groups."""
if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP:
return "mdi:google-circles-communities" # Matches isy scene icon 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.""" """Get whether the ISY994 switch program is on."""
return bool(self._node.status) 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.""" """Send the turn on command to the ISY994 switch program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to turn on switch") _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.""" """Send the turn off command to the ISY994 switch program."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to turn off switch") _LOGGER.error("Unable to turn off switch")

View File

@ -1,7 +1,12 @@
"""Provide info to system health.""" """Provide info to system health."""
from __future__ import annotations
from typing import Any
from pyisy import ISY from pyisy import ISY
from homeassistant.components import system_health from homeassistant.components import system_health
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -16,7 +21,7 @@ def async_register(
register.async_register_info(system_health_info) 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.""" """Get info for the info page."""
health_info = {} health_info = {}
@ -26,6 +31,7 @@ async def system_health_info(hass):
isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
entry = hass.config_entries.async_get_entry(config_entry_id) 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( health_info["host_reachable"] = await system_health.async_check_can_reach_url(
hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}" hass, f"{entry.data[CONF_HOST]}{ISY_URL_POSTFIX}"
) )

View File

@ -862,6 +862,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = 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.*] [mypy-homeassistant.components.iqvia.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -2275,45 +2286,6 @@ ignore_errors = true
[mypy-homeassistant.components.input_datetime] [mypy-homeassistant.components.input_datetime]
ignore_errors = true 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] [mypy-homeassistant.components.izone.climate]
ignore_errors = true ignore_errors = true

View File

@ -101,19 +101,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.icloud.sensor", "homeassistant.components.icloud.sensor",
"homeassistant.components.influxdb", "homeassistant.components.influxdb",
"homeassistant.components.input_datetime", "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.izone.climate",
"homeassistant.components.konnected", "homeassistant.components.konnected",
"homeassistant.components.konnected.config_flow", "homeassistant.components.konnected.config_flow",