Update ISY994 integration to be model agnostic (#85017)

This commit is contained in:
shbatm 2023-01-02 18:22:40 -06:00 committed by GitHub
parent 5d6ca6dd44
commit 240e1fd8f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 111 additions and 103 deletions

View file

@ -1,4 +1,4 @@
"""Support the ISY-994 controllers.""" """Support the Universal Devices ISY/IoX controllers."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -159,7 +159,7 @@ async def async_setup_entry(
port = host.port or 443 port = host.port or 443
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
else: else:
_LOGGER.error("The isy994 host value in configuration is invalid") _LOGGER.error("The ISY/IoX host value in configuration is invalid")
return False return False
# Connect to ISY controller. # Connect to ISY controller.
@ -310,7 +310,7 @@ async def async_remove_config_entry_device(
config_entry: config_entries.ConfigEntry, config_entry: config_entries.ConfigEntry,
device_entry: dr.DeviceEntry, device_entry: dr.DeviceEntry,
) -> bool: ) -> bool:
"""Remove isy994 config entry from a device.""" """Remove ISY config entry from a device."""
return not device_entry.identifiers.intersection( return not device_entry.identifiers.intersection(
(DOMAIN, unique_id) (DOMAIN, unique_id)
for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id) for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id)

View file

@ -1,4 +1,4 @@
"""Support for ISY994 binary sensors.""" """Support for ISY binary sensors."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -56,7 +56,7 @@ DEVICE_PARENT_REQUIRED = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 binary sensor platform.""" """Set up the ISY binary sensor platform."""
entities: list[ entities: list[
ISYInsteonBinarySensorEntity ISYInsteonBinarySensorEntity
| ISYBinarySensorEntity | ISYBinarySensorEntity
@ -219,7 +219,7 @@ def _detect_device_type_and_class(
class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
"""Representation of a basic ISY994 binary sensor device.""" """Representation of a basic ISY binary sensor device."""
def __init__( def __init__(
self, self,
@ -227,13 +227,13 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
force_device_class: BinarySensorDeviceClass | None = None, force_device_class: BinarySensorDeviceClass | None = None,
unknown_state: bool | None = None, unknown_state: bool | None = None,
) -> None: ) -> None:
"""Initialize the ISY994 binary sensor device.""" """Initialize the ISY binary sensor device."""
super().__init__(node) super().__init__(node)
self._device_class = force_device_class self._device_class = force_device_class
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Get whether the ISY994 binary sensor device is on.""" """Get whether the ISY binary sensor device is on."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return bool(self._node.status) return bool(self._node.status)
@ -248,7 +248,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
"""Representation of an ISY994 Insteon binary sensor device. """Representation of an ISY Insteon binary sensor device.
Often times, a single device is represented by multiple nodes in the ISY, Often times, a single device is represented by multiple nodes in the ISY,
allowing for different nuances in how those devices report their on and allowing for different nuances in how those devices report their on and
@ -262,7 +262,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
force_device_class: BinarySensorDeviceClass | None = None, force_device_class: BinarySensorDeviceClass | None = None,
unknown_state: bool | None = None, unknown_state: bool | None = None,
) -> None: ) -> None:
"""Initialize the ISY994 binary sensor device.""" """Initialize the ISY binary sensor device."""
super().__init__(node, force_device_class) super().__init__(node, force_device_class)
self._negative_node: Node | None = None self._negative_node: Node | None = None
self._heartbeat_device: ISYBinarySensorHeartbeat | None = None self._heartbeat_device: ISYBinarySensorHeartbeat | None = None
@ -374,7 +374,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Get whether the ISY994 binary sensor device is on. """Get whether the ISY binary sensor device is on.
Insteon leak sensors set their primary node to On when the state is Insteon leak sensors set their primary node to On when the state is
DRY, not WET, so we invert the binary state if the user indicates DRY, not WET, so we invert the binary state if the user indicates
@ -391,7 +391,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
"""Representation of the battery state of an ISY994 sensor.""" """Representation of the battery state of an ISY sensor."""
def __init__( def __init__(
self, self,
@ -401,7 +401,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
| ISYBinarySensorHeartbeat | ISYBinarySensorHeartbeat
| ISYBinarySensorProgramEntity, | ISYBinarySensorProgramEntity,
) -> None: ) -> None:
"""Initialize the ISY994 binary sensor device. """Initialize the ISY binary sensor device.
Computed state is set to UNKNOWN unless the ISY provided a valid Computed state is set to UNKNOWN unless the ISY provided a valid
state. See notes above regarding ISY Sensor status on ISY restart. state. See notes above regarding ISY Sensor status on ISY restart.
@ -479,7 +479,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on. """Get whether the ISY binary sensor device is on.
Note: This method will return false if the current state is UNKNOWN Note: This method will return false if the current state is UNKNOWN
which occurs after a restart until the first heartbeat or control which occurs after a restart until the first heartbeat or control
@ -501,7 +501,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity):
"""Representation of an ISY994 binary sensor program. """Representation of an ISY binary sensor program.
This does not need all of the subnode logic in the device version of binary This does not need all of the subnode logic in the device version of binary
sensors. sensors.
@ -509,5 +509,5 @@ class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on.""" """Get whether the ISY binary sensor device is on."""
return bool(self._node.status) return bool(self._node.status)

View file

@ -1,4 +1,4 @@
"""Support for Insteon Thermostats via ISY994 Platform.""" """Support for Insteon Thermostats via ISY Platform."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -56,7 +56,7 @@ from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 thermostat platform.""" """Set up the ISY thermostat platform."""
entities = [] entities = []
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
@ -68,7 +68,7 @@ async def async_setup_entry(
class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
"""Representation of an ISY994 thermostat entity.""" """Representation of an ISY thermostat entity."""
_attr_hvac_modes = ISY_HVAC_MODES _attr_hvac_modes = ISY_HVAC_MODES
_attr_precision = PRECISION_TENTHS _attr_precision = PRECISION_TENTHS

View file

@ -1,4 +1,4 @@
"""Config flow for Universal Devices ISY994 integration.""" """Config flow for Universal Devices ISY/IoX integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@ -79,7 +79,7 @@ async def validate_input(
port = host.port or HTTPS_PORT port = host.port or HTTPS_PORT
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
else: else:
_LOGGER.error("The isy994 host value in configuration is invalid") _LOGGER.error("The ISY/IoX host value in configuration is invalid")
raise InvalidHost raise InvalidHost
# Connect to ISY controller. # Connect to ISY controller.
@ -114,12 +114,12 @@ async def validate_input(
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Universal Devices ISY994.""" """Handle a config flow for Universal Devices ISY/IoX."""
VERSION = 1 VERSION = 1
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the isy994 config flow.""" """Initialize the ISY/IoX config flow."""
self.discovered_conf: dict[str, str] = {} self.discovered_conf: dict[str, str] = {}
self._existing_entry: config_entries.ConfigEntry | None = None self._existing_entry: config_entries.ConfigEntry | None = None
@ -200,7 +200,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
raise AbortFlow("already_configured") raise AbortFlow("already_configured")
async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
"""Handle a discovered isy994 via dhcp.""" """Handle a discovered ISY/IoX device via dhcp."""
friendly_name = discovery_info.hostname friendly_name = discovery_info.hostname
if friendly_name.startswith("polisy"): if friendly_name.startswith("polisy"):
url = f"http://{discovery_info.ip}:8080" url = f"http://{discovery_info.ip}:8080"
@ -221,7 +221,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user() return await self.async_step_user()
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a discovered isy994.""" """Handle a discovered ISY/IoX Device."""
friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]
url = discovery_info.ssdp_location url = discovery_info.ssdp_location
assert isinstance(url, str) assert isinstance(url, str)
@ -300,7 +300,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for isy994.""" """Handle a option flow for ISY/IoX."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None: def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""

View file

@ -1,4 +1,4 @@
"""Constants for the ISY994 Platform.""" """Constants for the ISY Platform."""
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass

View file

@ -1,4 +1,4 @@
"""Support for ISY994 covers.""" """Support for ISY covers."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast from typing import Any, cast
@ -30,7 +30,7 @@ from .helpers import migrate_old_unique_ids
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 cover platform.""" """Set up the ISY cover platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][COVER]: for node in hass_isy_data[ISY994_NODES][COVER]:
@ -44,7 +44,7 @@ async def async_setup_entry(
class ISYCoverEntity(ISYNodeEntity, CoverEntity): class ISYCoverEntity(ISYNodeEntity, CoverEntity):
"""Representation of an ISY994 cover device.""" """Representation of an ISY cover device."""
_attr_supported_features = ( _attr_supported_features = (
CoverEntityFeature.OPEN CoverEntityFeature.OPEN
@ -63,19 +63,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
@property @property
def is_closed(self) -> bool | None: def is_closed(self) -> bool | None:
"""Get whether the ISY994 cover device is closed.""" """Get whether the ISY cover device is closed."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return bool(self._node.status == 0) return bool(self._node.status == 0)
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Send the open cover command to the ISY994 cover device.""" """Send the open cover command to the ISY cover device."""
val = 100 if self._node.uom == UOM_BARRIER else None val = 100 if self._node.uom == UOM_BARRIER else None
if not await self._node.turn_on(val=val): if not await self._node.turn_on(val=val):
_LOGGER.error("Unable to open the cover") _LOGGER.error("Unable to open the cover")
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Send the close cover command to the ISY994 cover device.""" """Send the close cover command to the ISY cover device."""
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.error("Unable to close the cover") _LOGGER.error("Unable to close the cover")
@ -89,19 +89,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
"""Representation of an ISY994 cover program.""" """Representation of an ISY cover program."""
@property @property
def is_closed(self) -> bool: def is_closed(self) -> bool:
"""Get whether the ISY994 cover program is closed.""" """Get whether the ISY cover program is closed."""
return bool(self._node.status) return bool(self._node.status)
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Send the open cover command to the ISY994 cover program.""" """Send the open cover command to the ISY cover program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to open the cover") _LOGGER.error("Unable to open the cover")
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Send the close cover command to the ISY994 cover program.""" """Send the close cover command to the ISY cover program."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to close the cover") _LOGGER.error("Unable to close the cover")

View file

@ -33,7 +33,7 @@ from .const import DOMAIN
class ISYEntity(Entity): class ISYEntity(Entity):
"""Representation of an ISY994 device.""" """Representation of an ISY device."""
_name: str | None = None _name: str | None = None
_attr_should_poll = False _attr_should_poll = False
@ -56,12 +56,12 @@ class ISYEntity(Entity):
@callback @callback
def async_on_update(self, event: NodeProperty) -> None: def async_on_update(self, event: NodeProperty) -> None:
"""Handle the update event from the ISY994 Node.""" """Handle the update event from the ISY Node."""
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def async_on_control(self, event: NodeProperty) -> None: def async_on_control(self, event: NodeProperty) -> None:
"""Handle a control event from the ISY994 Node.""" """Handle a control event from the ISY Node."""
event_data = { event_data = {
"entity_id": self.entity_id, "entity_id": self.entity_id,
"control": event.control, "control": event.control,
@ -239,10 +239,10 @@ class ISYNodeEntity(ISYEntity):
class ISYProgramEntity(ISYEntity): class ISYProgramEntity(ISYEntity):
"""Representation of an ISY994 program base.""" """Representation of an ISY program base."""
def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: def __init__(self, name: str, status: Any | None, actions: Program = None) -> None:
"""Initialize the ISY994 program-based entity.""" """Initialize the ISY program-based entity."""
super().__init__(status) super().__init__(status)
self._name = name self._name = name
self._actions = actions self._actions = actions

View file

@ -1,4 +1,4 @@
"""Support for ISY994 fans.""" """Support for ISY fans."""
from __future__ import annotations from __future__ import annotations
import math import math
@ -26,7 +26,7 @@ SPEED_RANGE = (1, 255) # off is not included
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 fan platform.""" """Set up the ISY fan platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
entities: list[ISYFanEntity | ISYFanProgramEntity] = [] entities: list[ISYFanEntity | ISYFanProgramEntity] = []
@ -41,7 +41,7 @@ async def async_setup_entry(
class ISYFanEntity(ISYNodeEntity, FanEntity): class ISYFanEntity(ISYNodeEntity, FanEntity):
"""Representation of an ISY994 fan device.""" """Representation of an ISY fan device."""
_attr_supported_features = FanEntityFeature.SET_SPEED _attr_supported_features = FanEntityFeature.SET_SPEED
@ -67,7 +67,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
return bool(self._node.status != 0) return bool(self._node.status != 0)
async def async_set_percentage(self, percentage: int) -> None: async def async_set_percentage(self, percentage: int) -> None:
"""Set node to speed percentage for the ISY994 fan device.""" """Set node to speed percentage for the ISY fan device."""
if percentage == 0: if percentage == 0:
await self._node.turn_off() await self._node.turn_off()
return return
@ -82,16 +82,16 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
preset_mode: str | None = None, preset_mode: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Send the turn on command to the ISY994 fan device.""" """Send the turn on command to the ISY fan device."""
await self.async_set_percentage(percentage or 67) await self.async_set_percentage(percentage or 67)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the turn off command to the ISY994 fan device.""" """Send the turn off command to the ISY fan device."""
await self._node.turn_off() await self._node.turn_off()
class ISYFanProgramEntity(ISYProgramEntity, FanEntity): class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
"""Representation of an ISY994 fan program.""" """Representation of an ISY fan program."""
@property @property
def percentage(self) -> int | None: def percentage(self) -> int | None:
@ -111,7 +111,7 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
return bool(self._node.status != 0) return bool(self._node.status != 0)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the turn on command to ISY994 fan program.""" """Send the turn on command to ISY fan program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to turn off the fan") _LOGGER.error("Unable to turn off the fan")
@ -121,6 +121,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
preset_mode: str | None = None, preset_mode: str | None = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Send the turn off command to ISY994 fan program.""" """Send the turn off command to ISY fan program."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to turn on the fan") _LOGGER.error("Unable to turn on the fan")

View file

@ -1,4 +1,4 @@
"""Sorting helpers for ISY994 device classifications.""" """Sorting helpers for ISY device classifications."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
@ -327,7 +327,7 @@ def _categorize_nodes(
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
"""Categorize the ISY994 programs.""" """Categorize the ISY programs."""
for platform in PROGRAM_PLATFORMS: for platform in PROGRAM_PLATFORMS:
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
if not folder: if not folder:
@ -368,7 +368,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
def _categorize_variables( def _categorize_variables(
hass_isy_data: dict, variables: Variables, identifier: str hass_isy_data: dict, variables: Variables, identifier: str
) -> None: ) -> None:
"""Gather the ISY994 Variables to be added as sensors.""" """Gather the ISY Variables to be added as sensors."""
try: try:
var_to_add = [ var_to_add = [
(vtype, vname, vid) (vtype, vname, vid)

View file

@ -1,4 +1,4 @@
"""Support for ISY994 lights.""" """Support for ISY lights."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast from typing import Any, cast
@ -30,7 +30,7 @@ ATTR_LAST_BRIGHTNESS = "last_brightness"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 light platform.""" """Set up the ISY light platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
isy_options = entry.options isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
@ -45,27 +45,27 @@ async def async_setup_entry(
class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
"""Representation of an ISY994 light device.""" """Representation of an ISY light device."""
_attr_color_mode = ColorMode.BRIGHTNESS _attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(self, node: Node, restore_light_state: bool) -> None: def __init__(self, node: Node, restore_light_state: bool) -> None:
"""Initialize the ISY994 light device.""" """Initialize the ISY light device."""
super().__init__(node) super().__init__(node)
self._last_brightness: int | None = None self._last_brightness: int | None = None
self._restore_light_state = restore_light_state self._restore_light_state = restore_light_state
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 light is on.""" """Get whether the ISY light is on."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return False return False
return int(self._node.status) != 0 return int(self._node.status) != 0
@property @property
def brightness(self) -> int | None: def brightness(self) -> int | None:
"""Get the brightness of the ISY994 light.""" """Get the brightness of the ISY light."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
# Special Case for ISY Z-Wave Devices using % instead of 0-255: # Special Case for ISY Z-Wave Devices using % instead of 0-255:
@ -74,14 +74,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
return int(self._node.status) return int(self._node.status)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the turn off command to the ISY994 light device.""" """Send the turn off command to the ISY light device."""
self._last_brightness = self.brightness self._last_brightness = self.brightness
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.debug("Unable to turn off light") _LOGGER.debug("Unable to turn off light")
@callback @callback
def async_on_update(self, event: NodeProperty) -> None: def async_on_update(self, event: NodeProperty) -> None:
"""Save brightness in the update event from the ISY994 Node.""" """Save brightness in the update event from the ISY Node."""
if self._node.status not in (0, ISY_VALUE_UNKNOWN): if self._node.status not in (0, ISY_VALUE_UNKNOWN):
self._last_brightness = self._node.status self._last_brightness = self._node.status
if self._node.uom == UOM_PERCENTAGE: if self._node.uom == UOM_PERCENTAGE:
@ -91,7 +91,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
super().async_on_update(event) super().async_on_update(event)
async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None:
"""Send the turn on command to the ISY994 light device.""" """Send the turn on command to the ISY light device."""
if self._restore_light_state and brightness is None and self._last_brightness: if self._restore_light_state and brightness is None and self._last_brightness:
brightness = self._last_brightness brightness = self._last_brightness
# Special Case for ISY Z-Wave Devices using % instead of 0-255: # Special Case for ISY Z-Wave Devices using % instead of 0-255:

View file

@ -1,4 +1,4 @@
"""Support for ISY994 locks.""" """Support for ISY locks."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -20,7 +20,7 @@ VALUE_TO_STATE = {0: False, 100: True}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 lock platform.""" """Set up the ISY lock platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
entities: list[ISYLockEntity | ISYLockProgramEntity] = [] entities: list[ISYLockEntity | ISYLockProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][LOCK]: for node in hass_isy_data[ISY994_NODES][LOCK]:
@ -34,7 +34,7 @@ async def async_setup_entry(
class ISYLockEntity(ISYNodeEntity, LockEntity): class ISYLockEntity(ISYNodeEntity, LockEntity):
"""Representation of an ISY994 lock device.""" """Representation of an ISY lock device."""
@property @property
def is_locked(self) -> bool | None: def is_locked(self) -> bool | None:
@ -44,12 +44,12 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
return VALUE_TO_STATE.get(self._node.status) return VALUE_TO_STATE.get(self._node.status)
async def async_lock(self, **kwargs: Any) -> None: async def async_lock(self, **kwargs: Any) -> None:
"""Send the lock command to the ISY994 device.""" """Send the lock command to the ISY device."""
if not await self._node.secure_lock(): if not await self._node.secure_lock():
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
async def async_unlock(self, **kwargs: Any) -> None: async def async_unlock(self, **kwargs: Any) -> None:
"""Send the unlock command to the ISY994 device.""" """Send the unlock command to the ISY device."""
if not await self._node.secure_unlock(): if not await self._node.secure_unlock():
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")

View file

@ -1,6 +1,6 @@
{ {
"domain": "isy994", "domain": "isy994",
"name": "Universal Devices ISY994", "name": "Universal Devices ISY/IoX",
"integration_type": "hub", "integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/isy994", "documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.10"], "requirements": ["pyisy==3.0.10"],
@ -13,9 +13,17 @@
} }
], ],
"dhcp": [ "dhcp": [
{ "registered_devices": true }, {
{ "hostname": "isy*", "macaddress": "0021B9*" }, "registered_devices": true
{ "hostname": "polisy*", "macaddress": "000DB9*" } },
{
"hostname": "isy*",
"macaddress": "0021B9*"
},
{
"hostname": "polisy*",
"macaddress": "000DB9*"
}
], ],
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyisy"] "loggers": ["pyisy"]

View file

@ -1,4 +1,4 @@
"""Support for ISY994 sensors.""" """Support for ISY sensors."""
from __future__ import annotations from __future__ import annotations
from typing import Any, cast from typing import Any, cast
@ -81,7 +81,7 @@ ISY_CONTROL_TO_ENTITY_CATEGORY = {
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 sensor platform.""" """Set up the ISY sensor platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
@ -112,7 +112,7 @@ async def async_setup_entry(
class ISYSensorEntity(ISYNodeEntity, SensorEntity): class ISYSensorEntity(ISYNodeEntity, SensorEntity):
"""Representation of an ISY994 sensor device.""" """Representation of an ISY sensor device."""
@property @property
def target(self) -> Node | NodeProperty | None: def target(self) -> Node | NodeProperty | None:
@ -126,7 +126,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
@property @property
def raw_unit_of_measurement(self) -> dict | str | None: def raw_unit_of_measurement(self) -> dict | str | None:
"""Get the raw unit of measurement for the ISY994 sensor device.""" """Get the raw unit of measurement for the ISY sensor device."""
if self.target is None: if self.target is None:
return None return None
@ -148,7 +148,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
@property @property
def native_value(self) -> float | int | str | None: def native_value(self) -> float | int | str | None:
"""Get the state of the ISY994 sensor device.""" """Get the state of the ISY sensor device."""
if self.target is None: if self.target is None:
return None return None
@ -199,10 +199,10 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
class ISYAuxSensorEntity(ISYSensorEntity): class ISYAuxSensorEntity(ISYSensorEntity):
"""Representation of an ISY994 aux sensor device.""" """Representation of an ISY aux sensor device."""
def __init__(self, node: Node, control: str, enabled_default: bool) -> None: def __init__(self, node: Node, control: str, enabled_default: bool) -> None:
"""Initialize the ISY994 aux sensor.""" """Initialize the ISY aux sensor."""
super().__init__(node) super().__init__(node)
self._control = control self._control = control
self._attr_entity_registry_enabled_default = enabled_default self._attr_entity_registry_enabled_default = enabled_default
@ -239,10 +239,10 @@ class ISYAuxSensorEntity(ISYSensorEntity):
class ISYSensorVariableEntity(ISYEntity, SensorEntity): class ISYSensorVariableEntity(ISYEntity, SensorEntity):
"""Representation of an ISY994 variable as a sensor device.""" """Representation of an ISY variable as a sensor device."""
def __init__(self, vname: str, vobj: object) -> None: def __init__(self, vname: str, vobj: object) -> None:
"""Initialize the ISY994 binary sensor program.""" """Initialize the ISY binary sensor program."""
super().__init__(vobj) super().__init__(vobj)
self._name = vname self._name = vname

View file

@ -300,7 +300,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
_LOGGER.debug( _LOGGER.debug(
( (
"Cleaning up ISY994 Entities and devices: Config Entries: %s, Current" "Cleaning up ISY Entities and devices: Config Entries: %s, Current"
" Entries: %s, Extra Entries Removed: %s" " Entries: %s, Extra Entries Removed: %s"
), ),
len(config_ids), len(config_ids),
@ -309,7 +309,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
) )
async def async_reload_config_entries(service: ServiceCall) -> None: async def async_reload_config_entries(service: ServiceCall) -> None:
"""Trigger a reload of all ISY994 config entries.""" """Trigger a reload of all ISY config entries."""
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) hass.async_create_task(hass.config_entries.async_reload(config_entry_id))

View file

@ -1,4 +1,4 @@
# Describes the ISY994-specific services available # Describes the ISY-specific services available
# Note: controlling many entity_ids with one call is not recommended since it may result in # Note: controlling many entity_ids with one call is not recommended since it may result in
# flooding the ISY with requests. To control multiple devices with a service call # flooding the ISY with requests. To control multiple devices with a service call
@ -119,9 +119,9 @@ set_zwave_parameter:
- "2" - "2"
- "4" - "4"
rename_node: rename_node:
name: Rename Node on ISY994 name: Rename Node on ISY
description: >- description: >-
Rename a node or group (scene) on the ISY994. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. Rename a node or group (scene) on the ISY. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match.
The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the
name within Home Assistant. name within Home Assistant.
target: target:
@ -130,7 +130,7 @@ rename_node:
fields: fields:
name: name:
name: New Name name: New Name
description: The new name to use within the ISY994. description: The new name to use within the ISY.
required: true required: true
example: "Front Door Light" example: "Front Door Light"
selector: selector:
@ -291,7 +291,7 @@ run_network_resource:
text: text:
reload: reload:
name: Reload name: Reload
description: Reload the ISY994 connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. description: Reload the ISY connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY.
cleanup_entities: cleanup_entities:
name: Cleanup entities name: Cleanup entities
description: Cleanup old entities and devices no longer used by the ISY994 integrations. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. description: Cleanup old entities and devices no longer used by the ISY integration. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items.

View file

@ -1,4 +1,4 @@
"""Support for ISY994 switches.""" """Support for ISY switches."""
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@ -18,7 +18,7 @@ from .helpers import migrate_old_unique_ids
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY994 switch platform.""" """Set up the ISY switch platform."""
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
for node in hass_isy_data[ISY994_NODES][SWITCH]: for node in hass_isy_data[ISY994_NODES][SWITCH]:
@ -32,22 +32,22 @@ async def async_setup_entry(
class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
"""Representation of an ISY994 switch device.""" """Representation of an ISY switch device."""
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Get whether the ISY994 device is in the on state.""" """Get whether the ISY device is in the on state."""
if self._node.status == ISY_VALUE_UNKNOWN: if self._node.status == ISY_VALUE_UNKNOWN:
return None return None
return bool(self._node.status) return bool(self._node.status)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the turn off command to the ISY994 switch.""" """Send the turn off command to the ISY switch."""
if not await self._node.turn_off(): if not await self._node.turn_off():
_LOGGER.debug("Unable to turn off switch") _LOGGER.debug("Unable to turn off switch")
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Send the turn on command to the ISY994 switch.""" """Send the turn on command to the ISY switch."""
if not await self._node.turn_on(): if not await self._node.turn_on():
_LOGGER.debug("Unable to turn on switch") _LOGGER.debug("Unable to turn on switch")
@ -60,20 +60,20 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity):
"""A representation of an ISY994 program switch.""" """A representation of an ISY program switch."""
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 switch program is on.""" """Get whether the ISY switch program is on."""
return bool(self._node.status) return bool(self._node.status)
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Send the turn on command to the ISY994 switch program.""" """Send the turn on command to the ISY switch program."""
if not await self._actions.run_then(): if not await self._actions.run_then():
_LOGGER.error("Unable to turn on switch") _LOGGER.error("Unable to turn on switch")
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Send the turn off command to the ISY994 switch program.""" """Send the turn off command to the ISY switch program."""
if not await self._actions.run_else(): if not await self._actions.run_else():
_LOGGER.error("Unable to turn off switch") _LOGGER.error("Unable to turn off switch")

View file

@ -2543,7 +2543,7 @@
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
}, },
"isy994": { "isy994": {
"name": "Universal Devices ISY994", "name": "Universal Devices ISY/IoX",
"integration_type": "hub", "integration_type": "hub",
"config_flow": true, "config_flow": true,
"iot_class": "local_push" "iot_class": "local_push"

View file

@ -1 +1 @@
"""Tests for the Universal Devices ISY994 integration.""" """Tests for the Universal Devices ISY/IoX integration."""

View file

@ -1,4 +1,4 @@
"""Test the Universal Devices ISY994 config flow.""" """Test the Universal Devices ISY/IoX config flow."""
import re import re
from unittest.mock import patch from unittest.mock import patch

View file

@ -1,4 +1,4 @@
"""Test ISY994 system health.""" """Test ISY system health."""
import asyncio import asyncio
from unittest.mock import Mock from unittest.mock import Mock