mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Enable strict typing for isy994 (#65439)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
2f0d0998a2
commit
6c38a6b569
@ -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.*
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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: [],
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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}"
|
||||
)
|
||||
|
50
mypy.ini
50
mypy.ini
@ -862,6 +862,17 @@ no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.iqvia.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
@ -2275,45 +2286,6 @@ ignore_errors = true
|
||||
[mypy-homeassistant.components.input_datetime]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.binary_sensor]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.climate]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.config_flow]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.cover]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.entity]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.fan]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.helpers]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.light]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.lock]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.sensor]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.services]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.isy994.switch]
|
||||
ignore_errors = true
|
||||
|
||||
[mypy-homeassistant.components.izone.climate]
|
||||
ignore_errors = true
|
||||
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user