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_select.*
homeassistant.components.integration.*
homeassistant.components.isy994.*
homeassistant.components.iqvia.*
homeassistant.components.jellyfin.*
homeassistant.components.jewish_calendar.*

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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: [],

View File

@ -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")

View File

@ -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

View File

@ -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():

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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"

View File

@ -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()

View File

@ -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")

View File

@ -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}"
)

View File

@ -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

View File

@ -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",