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:
parent
7ac547a6e0
commit
4ec88b41dc
13 changed files with 268 additions and 245 deletions
|
@ -1,7 +1,7 @@
|
|||
"""Support the ISY-994 controllers."""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import PyISY
|
||||
from pyisy import ISY
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
|
@ -16,7 +16,6 @@ from homeassistant.helpers.typing import ConfigType
|
|||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_ENABLE_CLIMATE,
|
||||
CONF_IGNORE_STRING,
|
||||
CONF_SENSOR_STRING,
|
||||
CONF_TLS_VER,
|
||||
|
@ -25,11 +24,10 @@ from .const import (
|
|||
DOMAIN,
|
||||
ISY994_NODES,
|
||||
ISY994_PROGRAMS,
|
||||
ISY994_WEATHER,
|
||||
SUPPORTED_PLATFORMS,
|
||||
SUPPORTED_PROGRAM_PLATFORMS,
|
||||
)
|
||||
from .helpers import _categorize_nodes, _categorize_programs, _categorize_weather
|
||||
from .helpers import _categorize_nodes, _categorize_programs
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
@ -45,7 +43,6 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
vol.Optional(
|
||||
CONF_SENSOR_STRING, default=DEFAULT_SENSOR_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:
|
||||
hass.data[ISY994_NODES][platform] = []
|
||||
|
||||
hass.data[ISY994_WEATHER] = []
|
||||
|
||||
hass.data[ISY994_PROGRAMS] = {}
|
||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||
hass.data[ISY994_PROGRAMS][platform] = []
|
||||
|
@ -73,7 +68,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
host = urlparse(isy_config.get(CONF_HOST))
|
||||
ignore_identifier = isy_config.get(CONF_IGNORE_STRING)
|
||||
sensor_identifier = isy_config.get(CONF_SENSOR_STRING)
|
||||
enable_climate = isy_config.get(CONF_ENABLE_CLIMATE)
|
||||
|
||||
if host.scheme == "http":
|
||||
https = False
|
||||
|
@ -86,7 +80,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
return False
|
||||
|
||||
# Connect to ISY controller.
|
||||
isy = PyISY.ISY(
|
||||
isy = ISY(
|
||||
host.hostname,
|
||||
port,
|
||||
username=user,
|
||||
|
@ -101,9 +95,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
_categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier)
|
||||
_categorize_programs(hass, isy.programs)
|
||||
|
||||
if enable_climate and isy.configuration.get("Weather Information"):
|
||||
_categorize_weather(hass, isy.climate)
|
||||
|
||||
def stop(event: object) -> None:
|
||||
"""Stop ISY auto updates."""
|
||||
isy.auto_update = False
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
from datetime import timedelta
|
||||
from typing import Callable
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR,
|
||||
BinarySensorEntity,
|
||||
|
@ -22,14 +24,14 @@ def setup_platform(
|
|||
):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
devices_by_address = {}
|
||||
child_nodes = []
|
||||
|
||||
for node in hass.data[ISY994_NODES][BINARY_SENSOR]:
|
||||
if node.parent_node is None:
|
||||
device = ISYBinarySensorEntity(node)
|
||||
devices.append(device)
|
||||
devices_by_nid[node.nid] = device
|
||||
devices_by_address[node.address] = device
|
||||
else:
|
||||
# We'll process the child nodes last, to ensure all parent nodes
|
||||
# have been processed
|
||||
|
@ -37,17 +39,17 @@ def setup_platform(
|
|||
|
||||
for node in child_nodes:
|
||||
try:
|
||||
parent_device = devices_by_nid[node.parent_node.nid]
|
||||
parent_device = devices_by_address[node.parent_node.address]
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"Node %s has a parent node %s, but no device "
|
||||
"was created for the parent. Skipping.",
|
||||
node.nid,
|
||||
node.parent_nid,
|
||||
node.address,
|
||||
node.primary_node,
|
||||
)
|
||||
else:
|
||||
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"):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
|
@ -86,11 +88,6 @@ def _detect_device_type(node) -> str:
|
|||
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):
|
||||
"""Representation of an ISY994 binary sensor device.
|
||||
|
||||
|
@ -106,21 +103,21 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
|
|||
self._negative_node = None
|
||||
self._heartbeat_device = None
|
||||
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._status_was_unknown = True
|
||||
else:
|
||||
self._computed_state = bool(self._node.status._val)
|
||||
self._computed_state = bool(self._node.status)
|
||||
self._status_was_unknown = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the node and subnode event emitters."""
|
||||
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:
|
||||
self._negative_node.controlEvents.subscribe(
|
||||
self._negative_node.control_events.subscribe(
|
||||
self._negative_node_control_handler
|
||||
)
|
||||
|
||||
|
@ -146,20 +143,19 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
|
|||
"""
|
||||
self._negative_node = child
|
||||
|
||||
# pylint: disable=protected-access
|
||||
if not _is_val_unknown(self._negative_node.status._val):
|
||||
if self._negative_node.status != ISY_VALUE_UNKNOWN:
|
||||
# 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
|
||||
# negative and positive nodes disagree on the state (both ON or
|
||||
# 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
|
||||
# of the sensor until we receive our first ON event.
|
||||
self._computed_state = None
|
||||
|
||||
def _negative_node_control_handler(self, event: object) -> None:
|
||||
"""Handle an "On" control event from the "negative" node."""
|
||||
if event == "DON":
|
||||
if event.control == "DON":
|
||||
_LOGGER.debug(
|
||||
"Sensor %s turning Off via the Negative node sending a DON command",
|
||||
self.name,
|
||||
|
@ -175,7 +171,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
|
|||
will come to this node, with the negative node representing Off
|
||||
events
|
||||
"""
|
||||
if event == "DON":
|
||||
if event.control == "DON":
|
||||
_LOGGER.debug(
|
||||
"Sensor %s turning On via the Primary node sending a DON command",
|
||||
self.name,
|
||||
|
@ -183,7 +179,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
|
|||
self._computed_state = True
|
||||
self.schedule_update_ha_state()
|
||||
self._heartbeat()
|
||||
if event == "DOF":
|
||||
if event.control == "DOF":
|
||||
_LOGGER.debug(
|
||||
"Sensor %s turning Off via the Primary node sending a DOF command",
|
||||
self.name,
|
||||
|
@ -263,14 +259,14 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity):
|
|||
"""Subscribe to the node and subnode event emitters."""
|
||||
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
|
||||
self._restart_timer()
|
||||
|
||||
def _heartbeat_node_control_handler(self, event: object) -> None:
|
||||
"""Update the heartbeat timestamp when an On event is sent."""
|
||||
if event == "DON":
|
||||
if event.control == "DON":
|
||||
self.heartbeat()
|
||||
|
||||
def heartbeat(self):
|
||||
|
|
|
@ -37,6 +37,7 @@ from homeassistant.const import (
|
|||
LENGTH_INCHES,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_METERS,
|
||||
LENGTH_MILES,
|
||||
MASS_KILOGRAMS,
|
||||
MASS_POUNDS,
|
||||
POWER_WATT,
|
||||
|
@ -81,7 +82,6 @@ MANUFACTURER = "Universal Devices, Inc"
|
|||
|
||||
CONF_IGNORE_STRING = "ignore_string"
|
||||
CONF_SENSOR_STRING = "sensor_string"
|
||||
CONF_ENABLE_CLIMATE = "enable_climate"
|
||||
CONF_TLS_VER = "tls"
|
||||
|
||||
DEFAULT_IGNORE_STRING = "{IGNORE ME}"
|
||||
|
@ -89,8 +89,6 @@ DEFAULT_SENSOR_STRING = "sensor"
|
|||
DEFAULT_TLS_VERSION = 1.1
|
||||
|
||||
KEY_ACTIONS = "actions"
|
||||
KEY_FOLDER = "folder"
|
||||
KEY_MY_PROGRAMS = "My Programs"
|
||||
KEY_STATUS = "status"
|
||||
|
||||
SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH]
|
||||
|
@ -104,7 +102,6 @@ ISY_GROUP_PLATFORM = SWITCH
|
|||
|
||||
ISY994_ISY = "isy"
|
||||
ISY994_NODES = "isy994_nodes"
|
||||
ISY994_WEATHER = "isy994_weather"
|
||||
ISY994_PROGRAMS = "isy994_programs"
|
||||
|
||||
# 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,
|
||||
"91": DEGREE,
|
||||
"92": f"{DEGREE} South",
|
||||
"100": "", # Range 0-255, no unit.
|
||||
"101": f"{DEGREE} (x2)",
|
||||
"102": "kWs",
|
||||
"103": "$",
|
||||
"104": "¢",
|
||||
"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 = {
|
||||
|
@ -466,6 +477,21 @@ UOM_TO_STATES = {
|
|||
7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only
|
||||
},
|
||||
"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 = {
|
||||
|
@ -474,6 +500,3 @@ ISY_BIN_SENS_DEVICE_TYPES = {
|
|||
"motion": ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."],
|
||||
"climate": ["5.11.", "5.10."],
|
||||
}
|
||||
|
||||
# TEMPORARY CONSTANTS -- REMOVE AFTER PyISYv2 IS AVAILABLE
|
||||
ISY_VALUE_UNKNOWN = -1 * float("inf")
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
"""Support for ISY994 covers."""
|
||||
from typing import Callable
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
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 . import ISY994_NODES, ISY994_PROGRAMS
|
||||
|
@ -30,8 +32,8 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
|
|||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
"""Return the current cover position."""
|
||||
if self.is_unknown() or self.value is None:
|
||||
return None
|
||||
if self.value in [None, ISY_VALUE_UNKNOWN]:
|
||||
return STATE_UNKNOWN
|
||||
return sorted((0, self.value, 100))[1]
|
||||
|
||||
@property
|
||||
|
@ -42,19 +44,18 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity):
|
|||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 cover device."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
# TEMPORARY: Cast value to int until PyISYv2.
|
||||
return UOM_TO_STATES["97"].get(int(self.value), STATE_OPEN)
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return STATE_UNKNOWN
|
||||
return UOM_TO_STATES["97"].get(self.value, STATE_OPEN)
|
||||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
|
||||
|
@ -68,10 +69,10 @@ class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity):
|
|||
|
||||
def open_cover(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
def close_cover(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
"""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.typing import Dict
|
||||
|
||||
|
@ -7,39 +16,48 @@ from homeassistant.helpers.typing import Dict
|
|||
class ISYEntity(Entity):
|
||||
"""Representation of an ISY994 device."""
|
||||
|
||||
_attrs = {}
|
||||
_name: str = None
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the insteon device."""
|
||||
self._node = node
|
||||
self._attrs = {}
|
||||
self._change_handler = None
|
||||
self._control_handler = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""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"):
|
||||
self._control_handler = self._node.controlEvents.subscribe(self.on_control)
|
||||
if hasattr(self._node, "control_events"):
|
||||
self._control_handler = self._node.control_events.subscribe(self.on_control)
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
"""Handle the update event from the ISY994 Node."""
|
||||
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."""
|
||||
self.hass.bus.fire(
|
||||
"isy994_control", {"entity_id": self.entity_id, "control": event}
|
||||
)
|
||||
event_data = {
|
||||
"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
|
||||
def unique_id(self) -> str:
|
||||
"""Get the unique identifier of the device."""
|
||||
# pylint: disable=protected-access
|
||||
if hasattr(self._node, "_id"):
|
||||
return self._node._id
|
||||
|
||||
if hasattr(self._node, "address"):
|
||||
return self._node.address
|
||||
return None
|
||||
|
||||
@property
|
||||
|
@ -55,21 +73,13 @@ class ISYEntity(Entity):
|
|||
@property
|
||||
def value(self) -> int:
|
||||
"""Get the current value of the device."""
|
||||
# pylint: disable=protected-access
|
||||
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")
|
||||
return self._node.status
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the ISY device."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return STATE_UNKNOWN
|
||||
return super().state
|
||||
|
||||
|
||||
|
@ -78,12 +88,25 @@ class ISYNodeEntity(ISYEntity):
|
|||
|
||||
@property
|
||||
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 = {}
|
||||
if hasattr(self._node, "aux_properties"):
|
||||
for name, val in self._node.aux_properties.items():
|
||||
attr[name] = f"{val.get('value')} {val.get('uom')}"
|
||||
return attr
|
||||
# Cast as list due to RuntimeError if a new property is added while running.
|
||||
for name, value in list(self._node.aux_properties.items()):
|
||||
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):
|
||||
|
@ -94,3 +117,28 @@ class ISYProgramEntity(ISYEntity):
|
|||
super().__init__(status)
|
||||
self._name = name
|
||||
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
|
||||
|
|
|
@ -60,7 +60,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
|
|||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""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:
|
||||
"""Send the turn on command to the ISY994 fan device."""
|
||||
|
@ -68,7 +68,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity):
|
|||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 fan device."""
|
||||
self._node.off()
|
||||
self._node.turn_off()
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
|
@ -87,21 +87,19 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity):
|
|||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
# TEMPORARY: Cast value to int until PyISYv2.
|
||||
return VALUE_TO_STATE.get(int(self.value))
|
||||
return VALUE_TO_STATE.get(self.value)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get if the fan is on."""
|
||||
# TEMPORARY: Cast value to int until PyISYv2.
|
||||
return int(self.value) != 0
|
||||
return self.value != 0
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
def turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
"""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")
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""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.fan import DOMAIN as FAN
|
||||
|
@ -13,22 +15,17 @@ from .const import (
|
|||
_LOGGER,
|
||||
ISY994_NODES,
|
||||
ISY994_PROGRAMS,
|
||||
ISY994_WEATHER,
|
||||
ISY_GROUP_PLATFORM,
|
||||
KEY_ACTIONS,
|
||||
KEY_FOLDER,
|
||||
KEY_MY_PROGRAMS,
|
||||
KEY_STATUS,
|
||||
NODE_FILTERS,
|
||||
SUPPORTED_PLATFORMS,
|
||||
SUPPORTED_PROGRAM_PLATFORMS,
|
||||
)
|
||||
|
||||
WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom"))
|
||||
|
||||
|
||||
def _check_for_node_def(
|
||||
hass: HomeAssistantType, node, single_platform: str = None
|
||||
hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
|
||||
) -> bool:
|
||||
"""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(
|
||||
hass: HomeAssistantType, node, single_platform: str = None
|
||||
hass: HomeAssistantType, node: Union[Group, Node], single_platform: str = None
|
||||
) -> bool:
|
||||
"""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
|
||||
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:
|
||||
# Node doesn't have a type (non-Insteon device most likely)
|
||||
return False
|
||||
|
@ -77,7 +76,7 @@ def _check_for_insteon_type(
|
|||
# Hacky special-case just for FanLinc, which has a light module
|
||||
# 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
|
||||
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)
|
||||
return True
|
||||
|
||||
|
@ -88,7 +87,10 @@ def _check_for_insteon_type(
|
|||
|
||||
|
||||
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:
|
||||
"""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(
|
||||
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:
|
||||
"""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(
|
||||
hass: HomeAssistantType, nodes, ignore_identifier: str, sensor_identifier: str
|
||||
hass: HomeAssistantType,
|
||||
nodes: Nodes,
|
||||
ignore_identifier: str,
|
||||
sensor_identifier: str,
|
||||
) -> None:
|
||||
"""Sort the nodes to their proper platforms."""
|
||||
for (path, node) in nodes:
|
||||
|
@ -177,7 +185,7 @@ def _categorize_nodes(
|
|||
# Don't import this node as a device at all
|
||||
continue
|
||||
|
||||
if isinstance(node, Group):
|
||||
if hasattr(node, "protocol") and node.protocol == PROTO_GROUP:
|
||||
hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
|
||||
continue
|
||||
|
||||
|
@ -203,47 +211,37 @@ def _categorize_nodes(
|
|||
continue
|
||||
|
||||
|
||||
def _categorize_programs(hass: HomeAssistantType, programs: dict) -> None:
|
||||
def _categorize_programs(hass: HomeAssistantType, programs: Programs) -> None:
|
||||
"""Categorize the ISY994 programs."""
|
||||
for platform in SUPPORTED_PROGRAM_PLATFORMS:
|
||||
try:
|
||||
folder = programs[KEY_MY_PROGRAMS][f"HA.{platform}"]
|
||||
except KeyError:
|
||||
folder = programs.get_by_name(f"HA.{platform}")
|
||||
if not folder:
|
||||
continue
|
||||
|
||||
for dtype, _, node_id in folder.children:
|
||||
if dtype != KEY_FOLDER:
|
||||
if dtype != TAG_FOLDER:
|
||||
continue
|
||||
entity_folder = folder[node_id]
|
||||
try:
|
||||
status = entity_folder[KEY_STATUS]
|
||||
assert status.dtype == "program", "Not a program"
|
||||
if platform != BINARY_SENSOR:
|
||||
actions = entity_folder[KEY_ACTIONS]
|
||||
assert actions.dtype == "program", "Not a program"
|
||||
else:
|
||||
|
||||
actions = None
|
||||
except (AttributeError, KeyError, AssertionError):
|
||||
status = entity_folder.get_by_name(KEY_STATUS)
|
||||
if not status or not status.protocol == PROTO_PROGRAM:
|
||||
_LOGGER.warning(
|
||||
"Program entity '%s' not loaded due "
|
||||
"to invalid folder structure.",
|
||||
"Program %s entity '%s' not loaded, invalid/missing status program.",
|
||||
platform,
|
||||
entity_folder.name,
|
||||
)
|
||||
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)
|
||||
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)
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
"""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 (
|
||||
DOMAIN as LIGHT,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
@ -38,24 +41,24 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 light is on."""
|
||||
if self.is_unknown():
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return False
|
||||
return self.value != 0
|
||||
return int(self.value) != 0
|
||||
|
||||
@property
|
||||
def brightness(self) -> float:
|
||||
"""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:
|
||||
"""Send the turn off command to the ISY994 light device."""
|
||||
self._last_brightness = self.brightness
|
||||
if not self._node.off():
|
||||
if not self._node.turn_off():
|
||||
_LOGGER.debug("Unable to turn off light")
|
||||
|
||||
def on_update(self, event: object) -> None:
|
||||
"""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
|
||||
super().on_update(event)
|
||||
|
||||
|
@ -64,7 +67,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||
"""Send the turn on command to the ISY994 light device."""
|
||||
if brightness is None and 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")
|
||||
|
||||
@property
|
||||
|
@ -73,9 +76,11 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
|
|||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
def device_state_attributes(self) -> Dict:
|
||||
"""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:
|
||||
"""Restore last_brightness on restart."""
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Support for ISY994 locks."""
|
||||
from typing import Callable
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
from homeassistant.components.lock import DOMAIN as LOCK, LockEntity
|
||||
from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
@ -29,11 +31,6 @@ def setup_platform(
|
|||
class ISYLockEntity(ISYNodeEntity, LockEntity):
|
||||
"""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
|
||||
def is_locked(self) -> bool:
|
||||
"""Get whether the lock is in locked state."""
|
||||
|
@ -42,28 +39,20 @@ class ISYLockEntity(ISYNodeEntity, LockEntity):
|
|||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the lock."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return STATE_UNKNOWN
|
||||
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
|
||||
|
||||
def lock(self, **kwargs) -> None:
|
||||
"""Send the lock command to the ISY994 device."""
|
||||
# Hack until PyISY is updated
|
||||
req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "1"])
|
||||
response = self._conn.request(req_url)
|
||||
|
||||
if response is None:
|
||||
if not self._node.secure_lock():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
self._node.update(0.5)
|
||||
|
||||
def unlock(self, **kwargs) -> None:
|
||||
"""Send the unlock command to the ISY994 device."""
|
||||
# Hack until PyISY is updated
|
||||
req_url = self._conn.compileURL(["nodes", self.unique_id, "cmd", "SECMD", "0"])
|
||||
response = self._conn.request(req_url)
|
||||
|
||||
if response is None:
|
||||
if not self._node.secure_unlock():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
self._node.update(0.5)
|
||||
|
@ -84,10 +73,10 @@ class ISYLockProgramEntity(ISYProgramEntity, LockEntity):
|
|||
|
||||
def lock(self, **kwargs) -> None:
|
||||
"""Lock the device."""
|
||||
if not self._actions.runThen():
|
||||
if not self._actions.run_then():
|
||||
_LOGGER.error("Unable to lock device")
|
||||
|
||||
def unlock(self, **kwargs) -> None:
|
||||
"""Unlock the device."""
|
||||
if not self._actions.runElse():
|
||||
if not self._actions.run_else():
|
||||
_LOGGER.error("Unable to unlock device")
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
"domain": "isy994",
|
||||
"name": "Universal Devices ISY994",
|
||||
"documentation": "https://www.home-assistant.io/integrations/isy994",
|
||||
"requirements": ["PyISY==1.1.2"],
|
||||
"requirements": ["pyisy==2.0.2"],
|
||||
"codeowners": ["@bdraco", "@shbatm"]
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
"""Support for ISY994 sensors."""
|
||||
from typing import Callable
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN
|
||||
|
||||
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 . import ISY994_NODES, ISY994_WEATHER
|
||||
from . import ISY994_NODES
|
||||
from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES
|
||||
from .entity import ISYEntity, ISYNodeEntity
|
||||
from .entity import ISYNodeEntity
|
||||
|
||||
|
||||
def setup_platform(
|
||||
|
@ -20,9 +22,6 @@ def setup_platform(
|
|||
_LOGGER.debug("Loading %s", node.name)
|
||||
devices.append(ISYSensorEntity(node))
|
||||
|
||||
for node in hass.data[ISY994_WEATHER]:
|
||||
devices.append(ISYWeatherDevice(node))
|
||||
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
|
@ -32,28 +31,30 @@ class ISYSensorEntity(ISYNodeEntity):
|
|||
@property
|
||||
def raw_unit_of_measurement(self) -> str:
|
||||
"""Get the raw unit of measurement for the ISY994 sensor device."""
|
||||
if len(self._node.uom) == 1:
|
||||
if self._node.uom[0] in UOM_FRIENDLY_NAME:
|
||||
friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0])
|
||||
if friendly_name in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||
friendly_name = self.hass.config.units.temperature_unit
|
||||
return friendly_name
|
||||
return self._node.uom[0]
|
||||
return None
|
||||
uom = self._node.uom
|
||||
|
||||
# Backwards compatibility for ISYv4 Firmware:
|
||||
if isinstance(uom, list):
|
||||
return UOM_FRIENDLY_NAME.get(uom[0], uom[0])
|
||||
return UOM_FRIENDLY_NAME.get(uom)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 sensor device."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
if len(self._node.uom) == 1:
|
||||
if self._node.uom[0] in UOM_TO_STATES:
|
||||
states = UOM_TO_STATES.get(self._node.uom[0])
|
||||
# TEMPORARY: Cast value to int until PyISYv2.
|
||||
if int(self.value) in states:
|
||||
return states.get(int(self.value))
|
||||
elif self._node.prec and self._node.prec != [0]:
|
||||
uom = self._node.uom
|
||||
# Backwards compatibility for ISYv4 Firmware:
|
||||
if isinstance(uom, list):
|
||||
uom = uom[0]
|
||||
if not uom:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
states = UOM_TO_STATES.get(uom)
|
||||
if states and states.get(self.value):
|
||||
return states.get(self.value)
|
||||
if self._node.prec and int(self._node.prec) != 0:
|
||||
str_val = str(self.value)
|
||||
int_prec = int(self._node.prec)
|
||||
decimal_part = str_val[-int_prec:]
|
||||
|
@ -62,13 +63,9 @@ class ISYSensorEntity(ISYNodeEntity):
|
|||
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)
|
||||
else:
|
||||
return val
|
||||
return self.value
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Get the unit of measurement for the ISY994 sensor device."""
|
||||
|
@ -76,37 +73,3 @@ class ISYSensorEntity(ISYNodeEntity):
|
|||
if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
|
||||
return self.hass.config.units.temperature_unit
|
||||
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
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
"""Support for ISY994 switches."""
|
||||
from typing import Callable
|
||||
|
||||
from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import ISY994_NODES, ISY994_PROGRAMS
|
||||
|
@ -15,7 +18,6 @@ def setup_platform(
|
|||
"""Set up the ISY994 switch platform."""
|
||||
devices = []
|
||||
for node in hass.data[ISY994_NODES][SWITCH]:
|
||||
if not node.dimmable:
|
||||
devices.append(ISYSwitchEntity(node))
|
||||
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]:
|
||||
|
@ -30,18 +32,27 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):
|
|||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 device is in the on state."""
|
||||
if self.value == ISY_VALUE_UNKNOWN:
|
||||
return STATE_UNKNOWN
|
||||
return bool(self.value)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 switch."""
|
||||
if not self._node.off():
|
||||
_LOGGER.debug("Unable to turn on switch.")
|
||||
"""Send the turn off command to the ISY994 switch."""
|
||||
if not self._node.turn_off():
|
||||
_LOGGER.debug("Unable to turn off switch.")
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Send the turn off command to the ISY994 switch."""
|
||||
if not self._node.on():
|
||||
"""Send the turn on command to the ISY994 switch."""
|
||||
if not self._node.turn_on():
|
||||
_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):
|
||||
"""A representation of an ISY994 program switch."""
|
||||
|
@ -53,12 +64,12 @@ class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity):
|
|||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""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")
|
||||
|
||||
@property
|
||||
|
|
|
@ -49,9 +49,6 @@ PyEssent==0.13
|
|||
# homeassistant.components.github
|
||||
PyGithub==1.43.8
|
||||
|
||||
# homeassistant.components.isy994
|
||||
PyISY==1.1.2
|
||||
|
||||
# homeassistant.components.mvglive
|
||||
PyMVGLive==1.1.4
|
||||
|
||||
|
@ -1380,6 +1377,9 @@ pyirishrail==0.0.2
|
|||
# homeassistant.components.iss
|
||||
pyiss==1.0.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==2.0.2
|
||||
|
||||
# homeassistant.components.itach
|
||||
pyitachip2ir==0.0.7
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue