Migrate ISY994 to PyISY v2 (#35338)

* Remove unnecessary pylint exceptions

* Move up some change to binary_sensors and switch. Fix program d.s.a's.

* ISY994 Basic support for PyISYv2

- Bare minimum changes to be able to support PyISYv2.
- Renaming imports and functions to new names.
- Use necessary constants from module.
- **BREAKING CHANGE** Remove ISY Climate Module support.
    - Climate module was retired on 3/30/2020: [UDI Annoucement](https://www.universal-devices.com/byebyeclimatemodule/)
- **BREAKING CHANGE** Device State Attributes use NodeProperty
    - Some attributes names and types will have changed as part of the changes to PyISY. If a user relied on a device state attribute for a given entity, they should check that it is still there and formatted the same. In general, *more* state attributes should be getting picked up now that the underlying changes have been made.
- **BREAKING CHANGE** `isy994_control` event changes (using NodeProperty)
    - Control events now return an object with additional information. Control events are now parsed to the friendly names and will need to be updated in automations.
Remove cast

* PyISY v2.0.2, add extra UOMs, omit EMPTY_TIME attributes

* Fix typo in function doc string.

Co-authored-by: J. Nick Koston <nick@koston.org>

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
shbatm 2020-05-07 23:15:42 -05:00 committed by GitHub
parent 7ac547a6e0
commit 4ec88b41dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 268 additions and 245 deletions

View File

@ -1,7 +1,7 @@
"""Support the ISY-994 controllers.""" """Support the ISY-994 controllers."""
from urllib.parse import urlparse from urllib.parse import urlparse
import PyISY from pyisy import ISY
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -16,7 +16,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
_LOGGER, _LOGGER,
CONF_ENABLE_CLIMATE,
CONF_IGNORE_STRING, CONF_IGNORE_STRING,
CONF_SENSOR_STRING, CONF_SENSOR_STRING,
CONF_TLS_VER, CONF_TLS_VER,
@ -25,11 +24,10 @@ from .const import (
DOMAIN, DOMAIN,
ISY994_NODES, ISY994_NODES,
ISY994_PROGRAMS, ISY994_PROGRAMS,
ISY994_WEATHER,
SUPPORTED_PLATFORMS, SUPPORTED_PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS,
) )
from .helpers import _categorize_nodes, _categorize_programs, _categorize_weather from .helpers import _categorize_nodes, _categorize_programs
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -45,7 +43,6 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional( vol.Optional(
CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING
): cv.string, ): cv.string,
vol.Optional(CONF_ENABLE_CLIMATE, default=True): cv.boolean,
} }
) )
}, },
@ -59,8 +56,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
for platform in SUPPORTED_PLATFORMS: for platform in SUPPORTED_PLATFORMS:
hass.data[ISY994_NODES][platform] = [] hass.data[ISY994_NODES][platform] = []
hass.data[ISY994_WEATHER] = []
hass.data[ISY994_PROGRAMS] = {} hass.data[ISY994_PROGRAMS] = {}
for platform in SUPPORTED_PROGRAM_PLATFORMS: for platform in SUPPORTED_PROGRAM_PLATFORMS:
hass.data[ISY994_PROGRAMS][platform] = [] hass.data[ISY994_PROGRAMS][platform] = []
@ -73,7 +68,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
host = urlparse(isy_config.get(CONF_HOST)) host = urlparse(isy_config.get(CONF_HOST))
ignore_identifier = isy_config.get(CONF_IGNORE_STRING) ignore_identifier = isy_config.get(CONF_IGNORE_STRING)
sensor_identifier = isy_config.get(CONF_SENSOR_STRING) sensor_identifier = isy_config.get(CONF_SENSOR_STRING)
enable_climate = isy_config.get(CONF_ENABLE_CLIMATE)
if host.scheme == "http": if host.scheme == "http":
https = False https = False
@ -86,7 +80,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
return False return False
# Connect to ISY controller. # Connect to ISY controller.
isy = PyISY.ISY( isy = ISY(
host.hostname, host.hostname,
port, port,
username=user, username=user,
@ -101,9 +95,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
_categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass, isy.programs) _categorize_programs(hass, isy.programs)
if enable_climate and isy.configuration.get("Weather Information"):
_categorize_weather(hass, isy.climate)
def stop(event: object) -> None: def stop(event: object) -> None:
"""Stop ISY auto updates.""" """Stop ISY auto updates."""
isy.auto_update = False isy.auto_update = False

View File

@ -2,6 +2,8 @@
from datetime import timedelta from datetime import timedelta
from typing import Callable from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR, DOMAIN as BINARY_SENSOR,
BinarySensorEntity, BinarySensorEntity,
@ -22,14 +24,14 @@ def setup_platform(
): ):
"""Set up the ISY994 binary sensor platform.""" """Set up the ISY994 binary sensor platform."""
devices = [] devices = []
devices_by_nid = {} devices_by_address = {}
child_nodes = [] child_nodes = []
for node in hass.data[ISY994_NODES][BINARY_SENSOR]: for node in hass.data[ISY994_NODES][BINARY_SENSOR]:
if node.parent_node is None: if node.parent_node is None:
device = ISYBinarySensorEntity(node) device = ISYBinarySensorEntity(node)
devices.append(device) devices.append(device)
devices_by_nid[node.nid] = device devices_by_address[node.address] = device
else: else:
# We'll process the child nodes last, to ensure all parent nodes # We'll process the child nodes last, to ensure all parent nodes
# have been processed # have been processed
@ -37,17 +39,17 @@ def setup_platform(
for node in child_nodes: for node in child_nodes:
try: try:
parent_device = devices_by_nid[node.parent_node.nid] parent_device = devices_by_address[node.parent_node.address]
except KeyError: except KeyError:
_LOGGER.error( _LOGGER.error(
"Node %s has a parent node %s, but no device " "Node %s has a parent node %s, but no device "
"was created for the parent. Skipping.", "was created for the parent. Skipping.",
node.nid, node.address,
node.parent_nid, node.primary_node,
) )
else: else:
device_type = _detect_device_type(node) device_type = _detect_device_type(node)
subnode_id = int(node.nid[-1], 16) subnode_id = int(node.address[-1], 16)
if device_type in ("opening", "moisture"): if device_type in ("opening", "moisture"):
# These sensors use an optional "negative" subnode 2 to snag # These sensors use an optional "negative" subnode 2 to snag
# all state changes # all state changes
@ -86,11 +88,6 @@ def _detect_device_type(node) -> str:
return None return None
def _is_val_unknown(val):
"""Determine if a number value represents UNKNOWN from PyISY."""
return val == -1 * float("inf")
class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
"""Representation of an ISY994 binary sensor device. """Representation of an ISY994 binary sensor device.
@ -106,21 +103,21 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
self._negative_node = None self._negative_node = None
self._heartbeat_device = None self._heartbeat_device = None
self._device_class_from_type = _detect_device_type(self._node) self._device_class_from_type = _detect_device_type(self._node)
if _is_val_unknown(self._node.status._val): if self._node.status == ISY_VALUE_UNKNOWN:
self._computed_state = None self._computed_state = None
self._status_was_unknown = True self._status_was_unknown = True
else: else:
self._computed_state = bool(self._node.status._val) self._computed_state = bool(self._node.status)
self._status_was_unknown = False self._status_was_unknown = False
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to the node and subnode event emitters.""" """Subscribe to the node and subnode event emitters."""
await super().async_added_to_hass() await super().async_added_to_hass()
self._node.controlEvents.subscribe(self._positive_node_control_handler) self._node.control_events.subscribe(self._positive_node_control_handler)
if self._negative_node is not None: if self._negative_node is not None:
self._negative_node.controlEvents.subscribe( self._negative_node.control_events.subscribe(
self._negative_node_control_handler self._negative_node_control_handler
) )
@ -146,20 +143,19 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
""" """
self._negative_node = child self._negative_node = child
# pylint: disable=protected-access if self._negative_node.status != ISY_VALUE_UNKNOWN:
if not _is_val_unknown(self._negative_node.status._val):
# If the negative node has a value, it means the negative node is # If the negative node has a value, it means the negative node is
# in use for this device. Next we need to check to see if the # in use for this device. Next we need to check to see if the
# negative and positive nodes disagree on the state (both ON or # negative and positive nodes disagree on the state (both ON or
# both OFF). # both OFF).
if self._negative_node.status._val == self._node.status._val: if self._negative_node.status == self._node.status:
# The states disagree, therefore we cannot determine the state # The states disagree, therefore we cannot determine the state
# of the sensor until we receive our first ON event. # of the sensor until we receive our first ON event.
self._computed_state = None self._computed_state = None
def _negative_node_control_handler(self, event: object) -> None: def _negative_node_control_handler(self, event: object) -> None:
"""Handle an "On" control event from the "negative" node.""" """Handle an "On" control event from the "negative" node."""
if event == "DON": if event.control == "DON":
_LOGGER.debug( _LOGGER.debug(
"Sensor %s turning Off via the Negative node sending a DON command", "Sensor %s turning Off via the Negative node sending a DON command",
self.name, self.name,
@ -175,7 +171,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
will come to this node, with the negative node representing Off will come to this node, with the negative node representing Off
events events
""" """
if event == "DON": if event.control == "DON":
_LOGGER.debug( _LOGGER.debug(
"Sensor %s turning On via the Primary node sending a DON command", "Sensor %s turning On via the Primary node sending a DON command",
self.name, self.name,
@ -183,7 +179,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
self._computed_state = True self._computed_state = True
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._heartbeat() self._heartbeat()
if event == "DOF": if event.control == "DOF":
_LOGGER.debug( _LOGGER.debug(
"Sensor %s turning Off via the Primary node sending a DOF command", "Sensor %s turning Off via the Primary node sending a DOF command",
self.name, self.name,
@ -263,14 +259,14 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
"""Subscribe to the node and subnode event emitters.""" """Subscribe to the node and subnode event emitters."""
await super().async_added_to_hass() await super().async_added_to_hass()
self._node.controlEvents.subscribe(self._heartbeat_node_control_handler) self._node.control_events.subscribe(self._heartbeat_node_control_handler)
# Start the timer on bootup, so we can change from UNKNOWN to ON # Start the timer on bootup, so we can change from UNKNOWN to ON
self._restart_timer() self._restart_timer()
def _heartbeat_node_control_handler(self, event: object) -> None: def _heartbeat_node_control_handler(self, event: object) -> None:
"""Update the heartbeat timestamp when an On event is sent.""" """Update the heartbeat timestamp when an On event is sent."""
if event == "DON": if event.control == "DON":
self.heartbeat() self.heartbeat()
def heartbeat(self): def heartbeat(self):

View File

@ -37,6 +37,7 @@ from homeassistant.const import (
LENGTH_INCHES, LENGTH_INCHES,
LENGTH_KILOMETERS, LENGTH_KILOMETERS,
LENGTH_METERS, LENGTH_METERS,
LENGTH_MILES,
MASS_KILOGRAMS, MASS_KILOGRAMS,
MASS_POUNDS, MASS_POUNDS,
POWER_WATT, POWER_WATT,
@ -81,7 +82,6 @@ MANUFACTURER = "Universal Devices, Inc"
CONF_IGNORE_STRING = "ignore_string" CONF_IGNORE_STRING = "ignore_string"
CONF_SENSOR_STRING = "sensor_string" CONF_SENSOR_STRING = "sensor_string"
CONF_ENABLE_CLIMATE = "enable_climate"
CONF_TLS_VER = "tls" CONF_TLS_VER = "tls"
DEFAULT_IGNORE_STRING = "{IGNORE ME}" DEFAULT_IGNORE_STRING = "{IGNORE ME}"
@ -89,8 +89,6 @@ DEFAULT_SENSOR_STRING = "sensor"
DEFAULT_TLS_VERSION = 1.1 DEFAULT_TLS_VERSION = 1.1
KEY_ACTIONS = "actions" KEY_ACTIONS = "actions"
KEY_FOLDER = "folder"
KEY_MY_PROGRAMS = "My Programs"
KEY_STATUS = "status" KEY_STATUS = "status"
SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH] SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH]
@ -104,7 +102,6 @@ ISY_GROUP_PLATFORM = SWITCH
ISY994_ISY = "isy" ISY994_ISY = "isy"
ISY994_NODES = "isy994_nodes" ISY994_NODES = "isy994_nodes"
ISY994_WEATHER = "isy994_weather"
ISY994_PROGRAMS = "isy994_programs" ISY994_PROGRAMS = "isy994_programs"
# Do not use the Home Assistant consts for the states here - we're matching exact API # Do not use the Home Assistant consts for the states here - we're matching exact API
@ -288,12 +285,26 @@ UOM_FRIENDLY_NAME = {
"90": FREQUENCY_HERTZ, "90": FREQUENCY_HERTZ,
"91": DEGREE, "91": DEGREE,
"92": f"{DEGREE} South", "92": f"{DEGREE} South",
"100": "", # Range 0-255, no unit.
"101": f"{DEGREE} (x2)", "101": f"{DEGREE} (x2)",
"102": "kWs", "102": "kWs",
"103": "$", "103": "$",
"104": "¢", "104": "¢",
"105": LENGTH_INCHES, "105": LENGTH_INCHES,
"106": "mm/day", "106": f"mm/{TIME_DAYS}",
"107": "", # raw 1-byte unsigned value
"108": "", # raw 2-byte unsigned value
"109": "", # raw 3-byte unsigned value
"110": "", # raw 4-byte unsigned value
"111": "", # raw 1-byte signed value
"112": "", # raw 2-byte signed value
"113": "", # raw 3-byte signed value
"114": "", # raw 4-byte signed value
"116": LENGTH_MILES,
"117": "mb",
"118": "hPa",
"119": f"{POWER_WATT}{TIME_HOURS}",
"120": f"{LENGTH_INCHES}/{TIME_DAYS}",
} }
UOM_TO_STATES = { UOM_TO_STATES = {
@ -466,6 +477,21 @@ UOM_TO_STATES = {
7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only 7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only
}, },
"99": {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode "99": {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode
"115": { # Most recent On style action taken for lamp control
0: "on",
1: "off",
2: "fade up",
3: "fade down",
4: "fade stop",
5: "fast on",
6: "fast off",
7: "triple press on",
8: "triple press off",
9: "4x press on",
10: "4x press off",
11: "5x press on",
12: "5x press off",
},
} }
ISY_BIN_SENS_DEVICE_TYPES = { ISY_BIN_SENS_DEVICE_TYPES = {
@ -474,6 +500,3 @@ ISY_BIN_SENS_DEVICE_TYPES = {
"motion": ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], "motion": ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."],
"climate": ["5.11.", "5.10."], "climate": ["5.11.", "5.10."],
} }
# TEMPORARY CONSTANTS -- REMOVE AFTER PyISYv2 IS AVAILABLE
ISY_VALUE_UNKNOWN = -1 * float("inf")

View File

@ -1,8 +1,10 @@
"""Support for ISY994 covers.""" """Support for ISY994 covers."""
from typing import Callable from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.cover import DOMAIN as COVER, CoverEntity from homeassistant.components.cover import DOMAIN as COVER, CoverEntity
from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import ISY994_NODES, ISY994_PROGRAMS from . import ISY994_NODES, ISY994_PROGRAMS
@ -30,8 +32,8 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
@property @property
def current_cover_position(self) -> int: def current_cover_position(self) -> int:
"""Return the current cover position.""" """Return the current cover position."""
if self.is_unknown() or self.value is None: if self.value in [None, ISY_VALUE_UNKNOWN]:
return None return STATE_UNKNOWN
return sorted((0, self.value, 100))[1] return sorted((0, self.value, 100))[1]
@property @property
@ -42,19 +44,18 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the ISY994 cover device.""" """Get the state of the ISY994 cover device."""
if self.is_unknown(): if self.value == ISY_VALUE_UNKNOWN:
return None return STATE_UNKNOWN
# TEMPORARY: Cast value to int until PyISYv2. return UOM_TO_STATES["97"].get(self.value, STATE_OPEN)
return UOM_TO_STATES["97"].get(int(self.value), STATE_OPEN)
def open_cover(self, **kwargs) -> None: def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device.""" """Send the open cover command to the ISY994 cover device."""
if not self._node.on(val=100): if not self._node.turn_on(val=100):
_LOGGER.error("Unable to open the cover") _LOGGER.error("Unable to open the cover")
def close_cover(self, **kwargs) -> None: def close_cover(self, **kwargs) -> None:
"""Send the close cover command to the ISY994 cover device.""" """Send the close cover command to the ISY994 cover device."""
if not self._node.off(): if not self._node.turn_off():
_LOGGER.error("Unable to close the cover") _LOGGER.error("Unable to close the cover")
@ -68,10 +69,10 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
def open_cover(self, **kwargs) -> None: def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover program.""" """Send the open cover command to the ISY994 cover program."""
if not self._actions.runThen(): if not self._actions.run_then():
_LOGGER.error("Unable to open the cover") _LOGGER.error("Unable to open the cover")
def close_cover(self, **kwargs) -> None: def close_cover(self, **kwargs) -> None:
"""Send the close cover command to the ISY994 cover program.""" """Send the close cover command to the ISY994 cover program."""
if not self._actions.runElse(): if not self._actions.run_else():
_LOGGER.error("Unable to close the cover") _LOGGER.error("Unable to close the cover")

View File

@ -1,5 +1,14 @@
"""Representation of ISYEntity Types.""" """Representation of ISYEntity Types."""
from pyisy.constants import (
COMMAND_FRIENDLY_NAME,
EMPTY_TIME,
EVENT_PROPS_IGNORED,
ISY_VALUE_UNKNOWN,
)
from pyisy.helpers import NodeProperty
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import Dict from homeassistant.helpers.typing import Dict
@ -7,39 +16,48 @@ from homeassistant.helpers.typing import Dict
class ISYEntity(Entity): class ISYEntity(Entity):
"""Representation of an ISY994 device.""" """Representation of an ISY994 device."""
_attrs = {}
_name: str = None _name: str = None
def __init__(self, node) -> None: def __init__(self, node) -> None:
"""Initialize the insteon device.""" """Initialize the insteon device."""
self._node = node self._node = node
self._attrs = {}
self._change_handler = None self._change_handler = None
self._control_handler = None self._control_handler = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to the node change events.""" """Subscribe to the node change events."""
self._change_handler = self._node.status.subscribe("changed", self.on_update) self._change_handler = self._node.status_events.subscribe(self.on_update)
if hasattr(self._node, "controlEvents"): if hasattr(self._node, "control_events"):
self._control_handler = self._node.controlEvents.subscribe(self.on_control) self._control_handler = self._node.control_events.subscribe(self.on_control)
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node.""" """Handle the update event from the ISY994 Node."""
self.schedule_update_ha_state() self.schedule_update_ha_state()
def on_control(self, event: object) -> None: def on_control(self, event: NodeProperty) -> None:
"""Handle a control event from the ISY994 Node.""" """Handle a control event from the ISY994 Node."""
self.hass.bus.fire( event_data = {
"isy994_control", {"entity_id": self.entity_id, "control": event} "entity_id": self.entity_id,
) "control": event.control,
"value": event.value,
"formatted": event.formatted,
"uom": event.uom,
"precision": event.prec,
}
if event.value is None or event.control not in EVENT_PROPS_IGNORED:
# New state attributes may be available, update the state.
self.schedule_update_ha_state()
self.hass.bus.fire("isy994_control", event_data)
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique identifier of the device.""" """Get the unique identifier of the device."""
# pylint: disable=protected-access if hasattr(self._node, "address"):
if hasattr(self._node, "_id"): return self._node.address
return self._node._id
return None return None
@property @property
@ -55,21 +73,13 @@ class ISYEntity(Entity):
@property @property
def value(self) -> int: def value(self) -> int:
"""Get the current value of the device.""" """Get the current value of the device."""
# pylint: disable=protected-access return self._node.status
return self._node.status._val
def is_unknown(self) -> bool:
"""Get whether or not the value of this Entity's node is unknown.
PyISY reports unknown values as -inf
"""
return self.value == -1 * float("inf")
@property @property
def state(self): def state(self):
"""Return the state of the ISY device.""" """Return the state of the ISY device."""
if self.is_unknown(): if self.value == ISY_VALUE_UNKNOWN:
return None return STATE_UNKNOWN
return super().state return super().state
@ -78,12 +88,25 @@ class ISYNodeEntity(ISYEntity):
@property @property
def device_state_attributes(self) -> Dict: def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device.""" """Get the state attributes for the device.
The 'aux_properties' in the pyisy Node class are combined with the
other attributes which have been picked up from the event stream and
the combined result are returned as the device state attributes.
"""
attr = {} attr = {}
if hasattr(self._node, "aux_properties"): if hasattr(self._node, "aux_properties"):
for name, val in self._node.aux_properties.items(): # Cast as list due to RuntimeError if a new property is added while running.
attr[name] = f"{val.get('value')} {val.get('uom')}" for name, value in list(self._node.aux_properties.items()):
return attr attr_name = COMMAND_FRIENDLY_NAME.get(name, name)
attr[attr_name] = str(value.formatted).lower()
# If a Group/Scene, set a property if the entire scene is on/off
if hasattr(self._node, "group_all_on"):
attr["group_all_on"] = STATE_ON if self._node.group_all_on else STATE_OFF
self._attrs.update(attr)
return self._attrs
class ISYProgramEntity(ISYEntity): class ISYProgramEntity(ISYEntity):
@ -94,3 +117,28 @@ class ISYProgramEntity(ISYEntity):
super().__init__(status) super().__init__(status)
self._name = name self._name = name
self._actions = actions self._actions = actions
@property
def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
attr = {}
if self._actions:
attr["actions_enabled"] = self._actions.enabled
if self._actions.last_finished != EMPTY_TIME:
attr["actions_last_finished"] = self._actions.last_finished
if self._actions.last_run != EMPTY_TIME:
attr["actions_last_run"] = self._actions.last_run
if self._actions.last_update != EMPTY_TIME:
attr["actions_last_update"] = self._actions.last_update
attr["ran_else"] = self._actions.ran_else
attr["ran_then"] = self._actions.ran_then
attr["run_at_startup"] = self._actions.run_at_startup
attr["running"] = self._actions.running
attr["status_enabled"] = self._node.enabled
if self._node.last_finished != EMPTY_TIME:
attr["status_last_finished"] = self._node.last_finished
if self._node.last_run != EMPTY_TIME:
attr["status_last_run"] = self._node.last_run
if self._node.last_update != EMPTY_TIME:
attr["status_last_update"] = self._node.last_update
return attr

View File

@ -60,7 +60,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
def set_speed(self, speed: str) -> None: def set_speed(self, speed: str) -> None:
"""Send the set speed command to the ISY994 fan device.""" """Send the set speed command to the ISY994 fan device."""
self._node.on(val=STATE_TO_VALUE.get(speed, 255)) self._node.turn_on(val=STATE_TO_VALUE.get(speed, 255))
def turn_on(self, speed: str = None, **kwargs) -> None: def turn_on(self, speed: str = None, **kwargs) -> None:
"""Send the turn on command to the ISY994 fan device.""" """Send the turn on command to the ISY994 fan device."""
@ -68,7 +68,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 fan device.""" """Send the turn off command to the ISY994 fan device."""
self._node.off() self._node.turn_off()
@property @property
def speed_list(self) -> list: def speed_list(self) -> list:
@ -87,21 +87,19 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
@property @property
def speed(self) -> str: def speed(self) -> str:
"""Return the current speed.""" """Return the current speed."""
# TEMPORARY: Cast value to int until PyISYv2. return VALUE_TO_STATE.get(self.value)
return VALUE_TO_STATE.get(int(self.value))
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get if the fan is on.""" """Get if the fan is on."""
# TEMPORARY: Cast value to int until PyISYv2. return self.value != 0
return int(self.value) != 0
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn on command to ISY994 fan program.""" """Send the turn on command to ISY994 fan program."""
if not self._actions.runThen(): if not self._actions.run_then():
_LOGGER.error("Unable to turn off the fan") _LOGGER.error("Unable to turn off the fan")
def turn_on(self, speed: str = None, **kwargs) -> None: def turn_on(self, speed: str = None, **kwargs) -> None:
"""Send the turn off command to ISY994 fan program.""" """Send the turn off command to ISY994 fan program."""
if not self._actions.runElse(): if not self._actions.run_else():
_LOGGER.error("Unable to turn on the fan") _LOGGER.error("Unable to turn on the fan")

View File

@ -1,7 +1,9 @@
"""Sorting helpers for ISY994 device classifications.""" """Sorting helpers for ISY994 device classifications."""
from collections import namedtuple from typing import Union
from PyISY.Nodes import Group from pyisy.constants import PROTO_GROUP, PROTO_INSTEON, PROTO_PROGRAM, TAG_FOLDER
from pyisy.nodes import Group, Node, Nodes
from pyisy.programs import Programs
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.fan import DOMAIN as FAN
@ -13,22 +15,17 @@ from .const import (
_LOGGER, _LOGGER,
ISY994_NODES, ISY994_NODES,
ISY994_PROGRAMS, ISY994_PROGRAMS,
ISY994_WEATHER,
ISY_GROUP_PLATFORM, ISY_GROUP_PLATFORM,
KEY_ACTIONS, KEY_ACTIONS,
KEY_FOLDER,
KEY_MY_PROGRAMS,
KEY_STATUS, KEY_STATUS,
NODE_FILTERS, NODE_FILTERS,
SUPPORTED_PLATFORMS, SUPPORTED_PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS,
) )
WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom"))
def _check_for_node_def( def _check_for_node_def(
hass: HomeAssistantType, node, single_platform: str = None hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
) -> bool: ) -> bool:
"""Check if the node matches the node_def_id for any platforms. """Check if the node matches the node_def_id for any platforms.
@ -52,7 +49,7 @@ def _check_for_node_def(
def _check_for_insteon_type( def _check_for_insteon_type(
hass: HomeAssistantType, node, single_platform: str = None hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
) -> bool: ) -> bool:
"""Check if the node matches the Insteon type for any platforms. """Check if the node matches the Insteon type for any platforms.
@ -60,6 +57,8 @@ def _check_for_insteon_type(
works for Insteon device. "Node Server" (v5+) and Z-Wave and others will works for Insteon device. "Node Server" (v5+) and Z-Wave and others will
not have a type. not have a type.
""" """
if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON:
return False
if not hasattr(node, "type") or node.type is None: if not hasattr(node, "type") or node.type is None:
# Node doesn't have a type (non-Insteon device most likely) # Node doesn't have a type (non-Insteon device most likely)
return False return False
@ -77,7 +76,7 @@ def _check_for_insteon_type(
# Hacky special-case just for FanLinc, which has a light module # Hacky special-case just for FanLinc, which has a light module
# as one of its nodes. Note that this special-case is not necessary # as one of its nodes. Note that this special-case is not necessary
# on ISY 5.x firmware as it uses the superior NodeDefs method # on ISY 5.x firmware as it uses the superior NodeDefs method
if platform == FAN and int(node.nid[-1]) == 1: if platform == FAN and int(node.address[-1]) == 1:
hass.data[ISY994_NODES][LIGHT].append(node) hass.data[ISY994_NODES][LIGHT].append(node)
return True return True
@ -88,7 +87,10 @@ def _check_for_insteon_type(
def _check_for_uom_id( def _check_for_uom_id(
hass: HomeAssistantType, node, single_platform: str = None, uom_list: list = None hass: HomeAssistantType,
node: Union[Group, Node],
single_platform: str = None,
uom_list: list = None,
) -> bool: ) -> bool:
"""Check if a node's uom matches any of the platforms uom filter. """Check if a node's uom matches any of the platforms uom filter.
@ -116,7 +118,10 @@ def _check_for_uom_id(
def _check_for_states_in_uom( def _check_for_states_in_uom(
hass: HomeAssistantType, node, single_platform: str = None, states_list: list = None hass: HomeAssistantType,
node: Union[Group, Node],
single_platform: str = None,
states_list: list = None,
) -> bool: ) -> bool:
"""Check if a list of uoms matches two possible filters. """Check if a list of uoms matches two possible filters.
@ -168,7 +173,10 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistantType, node) -> bool:
def _categorize_nodes( def _categorize_nodes(
hass: HomeAssistantType, nodes, ignore_identifier: str, sensor_identifier: str hass: HomeAssistantType,
nodes: Nodes,
ignore_identifier: str,
sensor_identifier: str,
) -> None: ) -> None:
"""Sort the nodes to their proper platforms.""" """Sort the nodes to their proper platforms."""
for (path, node) in nodes: for (path, node) in nodes:
@ -177,7 +185,7 @@ def _categorize_nodes(
# Don't import this node as a device at all # Don't import this node as a device at all
continue continue
if isinstance(node, Group): if hasattr(node, "protocol") and node.protocol == PROTO_GROUP:
hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
continue continue
@ -203,47 +211,37 @@ def _categorize_nodes(
continue continue
def _categorize_programs(hass: HomeAssistantType, programs: dict) -> None: def _categorize_programs(hass: HomeAssistantType, programs: Programs) -> None:
"""Categorize the ISY994 programs.""" """Categorize the ISY994 programs."""
for platform in SUPPORTED_PROGRAM_PLATFORMS: for platform in SUPPORTED_PROGRAM_PLATFORMS:
try: folder = programs.get_by_name(f"HA.{platform}")
folder = programs[KEY_MY_PROGRAMS][f"HA.{platform}"] if not folder:
except KeyError:
continue continue
for dtype, _, node_id in folder.children: for dtype, _, node_id in folder.children:
if dtype != KEY_FOLDER: if dtype != TAG_FOLDER:
continue continue
entity_folder = folder[node_id] entity_folder = folder[node_id]
try:
status = entity_folder[KEY_STATUS] actions = None
assert status.dtype == "program", "Not a program" status = entity_folder.get_by_name(KEY_STATUS)
if platform != BINARY_SENSOR: if not status or not status.protocol == PROTO_PROGRAM:
actions = entity_folder[KEY_ACTIONS]
assert actions.dtype == "program", "Not a program"
else:
actions = None
except (AttributeError, KeyError, AssertionError):
_LOGGER.warning( _LOGGER.warning(
"Program entity '%s' not loaded due " "Program %s entity '%s' not loaded, invalid/missing status program.",
"to invalid folder structure.", platform,
entity_folder.name, entity_folder.name,
) )
continue continue
if platform != BINARY_SENSOR:
actions = entity_folder.get_by_name(KEY_ACTIONS)
if not actions or not actions.protocol == PROTO_PROGRAM:
_LOGGER.warning(
"Program %s entity '%s' not loaded, invalid/missing actions program.",
platform,
entity_folder.name,
)
continue
entity = (entity_folder.name, status, actions) entity = (entity_folder.name, status, actions)
hass.data[ISY994_PROGRAMS][platform].append(entity) hass.data[ISY994_PROGRAMS][platform].append(entity)
def _categorize_weather(hass: HomeAssistantType, climate) -> None:
"""Categorize the ISY994 weather data."""
climate_attrs = dir(climate)
weather_nodes = [
WeatherNode(
getattr(climate, attr),
attr.replace("_", " "),
getattr(climate, f"{attr}_units"),
)
for attr in climate_attrs
if f"{attr}_units" in climate_attrs
]
hass.data[ISY994_WEATHER].extend(weather_nodes)

View File

@ -1,11 +1,14 @@
"""Support for ISY994 lights.""" """Support for ISY994 lights."""
from typing import Callable from typing import Callable, Dict
from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.light import ( from homeassistant.components.light import (
DOMAIN as LIGHT, DOMAIN as LIGHT,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
LightEntity, LightEntity,
) )
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -38,24 +41,24 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 light is on.""" """Get whether the ISY994 light is on."""
if self.is_unknown(): if self.value == ISY_VALUE_UNKNOWN:
return False return False
return self.value != 0 return int(self.value) != 0
@property @property
def brightness(self) -> float: def brightness(self) -> float:
"""Get the brightness of the ISY994 light.""" """Get the brightness of the ISY994 light."""
return None if self.is_unknown() else self.value return STATE_UNKNOWN if self.value == ISY_VALUE_UNKNOWN else int(self.value)
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device.""" """Send the turn off command to the ISY994 light device."""
self._last_brightness = self.brightness self._last_brightness = self.brightness
if not self._node.off(): if not self._node.turn_off():
_LOGGER.debug("Unable to turn off light") _LOGGER.debug("Unable to turn off light")
def on_update(self, event: object) -> None: def on_update(self, event: object) -> None:
"""Save brightness in the update event from the ISY994 Node.""" """Save brightness in the update event from the ISY994 Node."""
if not self.is_unknown() and self.value != 0: if self.value not in (0, ISY_VALUE_UNKNOWN):
self._last_brightness = self.value self._last_brightness = self.value
super().on_update(event) super().on_update(event)
@ -64,7 +67,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
"""Send the turn on command to the ISY994 light device.""" """Send the turn on command to the ISY994 light device."""
if brightness is None and self._last_brightness: if brightness is None and self._last_brightness:
brightness = self._last_brightness brightness = self._last_brightness
if not self._node.on(val=brightness): if not self._node.turn_on(val=brightness):
_LOGGER.debug("Unable to turn on light") _LOGGER.debug("Unable to turn on light")
@property @property
@ -73,9 +76,11 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
@property @property
def device_state_attributes(self): def device_state_attributes(self) -> Dict:
"""Return the light attributes.""" """Return the light attributes."""
return {ATTR_LAST_BRIGHTNESS: self._last_brightness} attribs = super().device_state_attributes
attribs[ATTR_LAST_BRIGHTNESS] = self._last_brightness
return attribs
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Restore last_brightness on restart.""" """Restore last_brightness on restart."""

View File

@ -1,6 +1,8 @@
"""Support for ISY994 locks.""" """Support for ISY994 locks."""
from typing import Callable from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.lock import DOMAIN as LOCK, LockEntity from homeassistant.components.lock import DOMAIN as LOCK, LockEntity
from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -29,11 +31,6 @@ def setup_platform(
class ISYLockEntity(ISYNodeEntity, LockEntity): class ISYLockEntity(ISYNodeEntity, LockEntity):
"""Representation of an ISY994 lock device.""" """Representation of an ISY994 lock device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 lock device."""
super().__init__(node)
self._conn = node.parent.parent.conn
@property @property
def is_locked(self) -> bool: def is_locked(self) -> bool:
"""Get whether the lock is in locked state.""" """Get whether the lock is in locked state."""
@ -42,28 +39,20 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the lock.""" """Get the state of the lock."""
if self.is_unknown(): if self.value == ISY_VALUE_UNKNOWN:
return None return STATE_UNKNOWN
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN) return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None: def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device.""" """Send the lock command to the ISY994 device."""
# Hack until PyISY is updated if not self._node.secure_lock():
req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "1"])
response = self._conn.request(req_url)
if response is None:
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
self._node.update(0.5) self._node.update(0.5)
def unlock(self, **kwargs) -> None: def unlock(self, **kwargs) -> None:
"""Send the unlock command to the ISY994 device.""" """Send the unlock command to the ISY994 device."""
# Hack until PyISY is updated if not self._node.secure_unlock():
req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "0"])
response = self._conn.request(req_url)
if response is None:
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
self._node.update(0.5) self._node.update(0.5)
@ -84,10 +73,10 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
def lock(self, **kwargs) -> None: def lock(self, **kwargs) -> None:
"""Lock the device.""" """Lock the device."""
if not self._actions.runThen(): if not self._actions.run_then():
_LOGGER.error("Unable to lock device") _LOGGER.error("Unable to lock device")
def unlock(self, **kwargs) -> None: def unlock(self, **kwargs) -> None:
"""Unlock the device.""" """Unlock the device."""
if not self._actions.runElse(): if not self._actions.run_else():
_LOGGER.error("Unable to unlock device") _LOGGER.error("Unable to unlock device")

View File

@ -2,6 +2,6 @@
"domain": "isy994", "domain": "isy994",
"name": "Universal Devices ISY994", "name": "Universal Devices ISY994",
"documentation": "https://www.home-assistant.io/integrations/isy994", "documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["PyISY==1.1.2"], "requirements": ["pyisy==2.0.2"],
"codeowners": ["@bdraco", "@shbatm"] "codeowners": ["@bdraco", "@shbatm"]
} }

View File

@ -1,13 +1,15 @@
"""Support for ISY994 sensors.""" """Support for ISY994 sensors."""
from typing import Callable from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN
from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import ISY994_NODES, ISY994_WEATHER from . import ISY994_NODES
from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES
from .entity import ISYEntity, ISYNodeEntity from .entity import ISYNodeEntity
def setup_platform( def setup_platform(
@ -20,9 +22,6 @@ def setup_platform(
_LOGGER.debug("Loading %s", node.name) _LOGGER.debug("Loading %s", node.name)
devices.append(ISYSensorEntity(node)) devices.append(ISYSensorEntity(node))
for node in hass.data[ISY994_WEATHER]:
devices.append(ISYWeatherDevice(node))
add_entities(devices) add_entities(devices)
@ -32,42 +31,40 @@ class ISYSensorEntity(ISYNodeEntity):
@property @property
def raw_unit_of_measurement(self) -> str: def raw_unit_of_measurement(self) -> str:
"""Get the raw unit of measurement for the ISY994 sensor device.""" """Get the raw unit of measurement for the ISY994 sensor device."""
if len(self._node.uom) == 1: uom = self._node.uom
if self._node.uom[0] in UOM_FRIENDLY_NAME:
friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0]) # Backwards compatibility for ISYv4 Firmware:
if friendly_name in (TEMP_CELSIUS, TEMP_FAHRENHEIT): if isinstance(uom, list):
friendly_name = self.hass.config.units.temperature_unit return UOM_FRIENDLY_NAME.get(uom[0], uom[0])
return friendly_name return UOM_FRIENDLY_NAME.get(uom)
return self._node.uom[0]
return None
@property @property
def state(self) -> str: def state(self) -> str:
"""Get the state of the ISY994 sensor device.""" """Get the state of the ISY994 sensor device."""
if self.is_unknown(): if self.value == ISY_VALUE_UNKNOWN:
return None return STATE_UNKNOWN
if len(self._node.uom) == 1: uom = self._node.uom
if self._node.uom[0] in UOM_TO_STATES: # Backwards compatibility for ISYv4 Firmware:
states = UOM_TO_STATES.get(self._node.uom[0]) if isinstance(uom, list):
# TEMPORARY: Cast value to int until PyISYv2. uom = uom[0]
if int(self.value) in states: if not uom:
return states.get(int(self.value)) return STATE_UNKNOWN
elif self._node.prec and self._node.prec != [0]:
str_val = str(self.value)
int_prec = int(self._node.prec)
decimal_part = str_val[-int_prec:]
whole_part = str_val[: len(str_val) - int_prec]
val = float(f"{whole_part}.{decimal_part}")
raw_units = self.raw_unit_of_measurement
if raw_units in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
val = self.hass.config.units.temperature(val, raw_units)
return str(val) states = UOM_TO_STATES.get(uom)
else: if states and states.get(self.value):
return self.value return states.get(self.value)
if self._node.prec and int(self._node.prec) != 0:
return None str_val = str(self.value)
int_prec = int(self._node.prec)
decimal_part = str_val[-int_prec:]
whole_part = str_val[: len(str_val) - int_prec]
val = float(f"{whole_part}.{decimal_part}")
raw_units = self.raw_unit_of_measurement
if raw_units in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
val = self.hass.config.units.temperature(val, raw_units)
return val
return self.value
@property @property
def unit_of_measurement(self) -> str: def unit_of_measurement(self) -> str:
@ -76,37 +73,3 @@ class ISYSensorEntity(ISYNodeEntity):
if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS): if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
return self.hass.config.units.temperature_unit return self.hass.config.units.temperature_unit
return raw_units return raw_units
# Depreciated, not renaming. Will be removed in next PR.
class ISYWeatherDevice(ISYEntity):
"""Representation of an ISY994 weather device."""
@property
def raw_units(self) -> str:
"""Return the raw unit of measurement."""
if self._node.uom == "F":
return TEMP_FAHRENHEIT
if self._node.uom == "C":
return TEMP_CELSIUS
return self._node.uom
@property
def state(self) -> object:
"""Return the value of the node."""
# pylint: disable=protected-access
val = self._node.status._val
raw_units = self._node.uom
if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
return self.hass.config.units.temperature(val, raw_units)
return val
@property
def unit_of_measurement(self) -> str:
"""Return the unit of measurement for the node."""
raw_units = self.raw_units
if raw_units in [TEMP_CELSIUS, TEMP_FAHRENHEIT]:
return self.hass.config.units.temperature_unit
return raw_units

View File

@ -1,7 +1,10 @@
"""Support for ISY994 switches.""" """Support for ISY994 switches."""
from typing import Callable from typing import Callable
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import ISY994_NODES, ISY994_PROGRAMS from . import ISY994_NODES, ISY994_PROGRAMS
@ -15,8 +18,7 @@ def setup_platform(
"""Set up the ISY994 switch platform.""" """Set up the ISY994 switch platform."""
devices = [] devices = []
for node in hass.data[ISY994_NODES][SWITCH]: for node in hass.data[ISY994_NODES][SWITCH]:
if not node.dimmable: devices.append(ISYSwitchEntity(node))
devices.append(ISYSwitchEntity(node))
for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]: for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]:
devices.append(ISYSwitchProgramEntity(name, status, actions)) devices.append(ISYSwitchProgramEntity(name, status, actions))
@ -30,18 +32,27 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Get whether the ISY994 device is in the on state.""" """Get whether the ISY994 device is in the on state."""
if self.value == ISY_VALUE_UNKNOWN:
return STATE_UNKNOWN
return bool(self.value) return bool(self.value)
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch.""" """Send the turn off command to the ISY994 switch."""
if not self._node.off(): if not self._node.turn_off():
_LOGGER.debug("Unable to turn on switch.") _LOGGER.debug("Unable to turn off switch.")
def turn_on(self, **kwargs) -> None: def turn_on(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 switch.""" """Send the turn on command to the ISY994 switch."""
if not self._node.on(): if not self._node.turn_on():
_LOGGER.debug("Unable to turn on switch.") _LOGGER.debug("Unable to turn on switch.")
@property
def icon(self) -> str:
"""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
return super().icon
class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity):
"""A representation of an ISY994 program switch.""" """A representation of an ISY994 program switch."""
@ -53,12 +64,12 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity):
def turn_on(self, **kwargs) -> None: def turn_on(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch program.""" """Send the turn on command to the ISY994 switch program."""
if not self._actions.runThen(): if not self._actions.run_then():
_LOGGER.error("Unable to turn on switch") _LOGGER.error("Unable to turn on switch")
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 switch program.""" """Send the turn off command to the ISY994 switch program."""
if not self._actions.runElse(): if not self._actions.run_else():
_LOGGER.error("Unable to turn off switch") _LOGGER.error("Unable to turn off switch")
@property @property

View File

@ -49,9 +49,6 @@ PyEssent==0.13
# homeassistant.components.github # homeassistant.components.github
PyGithub==1.43.8 PyGithub==1.43.8
# homeassistant.components.isy994
PyISY==1.1.2
# homeassistant.components.mvglive # homeassistant.components.mvglive
PyMVGLive==1.1.4 PyMVGLive==1.1.4
@ -1380,6 +1377,9 @@ pyirishrail==0.0.2
# homeassistant.components.iss # homeassistant.components.iss
pyiss==1.0.1 pyiss==1.0.1
# homeassistant.components.isy994
pyisy==2.0.2
# homeassistant.components.itach # homeassistant.components.itach
pyitachip2ir==0.0.7 pyitachip2ir==0.0.7