Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware (#11243)
* Huge ISY994 platform cleanup, fixes support for 5.0.10 firmware # * No more globals - store on hass.data # * Parent ISY994 component handles categorizing nodes in to Hass components, rather than each individual domain filtering all nodes themselves # * Remove hidden string, replace with ignore string. Hidden should be done via the customize block; ignore fully prevents the node from getting a Hass entity # * Removed a few unused methods in the ISYDevice class # * Cleaned up the hostname parsing # * Removed broken logic in the fan Program component. It was setting properties that have no setters # * Added the missing SUPPORTED_FEATURES to the fan component to indicate that it can set speed # * Added better error handling and a log warning when an ISY994 program entity fails to initialize # * Cleaned up a few instances of unecessarily complicated logic paths, and other cases of unnecessary logic that is already handled by base classes * Use `super()` instead of explicit base class calls * Move `hass` argument to first position * Use str.format instead of string addition * Move program structure building and validation to component Removes the need for a bunch of duplicate exception handling in each individual platform * Fix climate nodes, fix climate names, add config to disable climate Sensor platform was crashing when the ISY reported climate nodes. Logic has been fixed. Also added a config option to prevent climate sensors from getting imported from the ISY. Also replace the underscore from climate node names with spaces so they default to friendly names. * Space missing in error message * Fix string comparison to use `==` * Explicitly check for attributes rather than catch AttributeError Also removes two stray debug lines * Remove null checks on hass.data, as they are always null at this point
This commit is contained in:
parent
a59b02b6b4
commit
d687bc073e
8 changed files with 358 additions and 348 deletions
|
@ -12,7 +12,8 @@ from typing import Callable # noqa
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
|
@ -20,9 +21,6 @@ from homeassistant.util import dt as dt_util
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
ISY_DEVICE_TYPES = {
|
||||
'moisture': ['16.8', '16.13', '16.14'],
|
||||
'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
|
||||
|
@ -34,16 +32,11 @@ ISY_DEVICE_TYPES = {
|
|||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
child_nodes = []
|
||||
|
||||
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
|
||||
states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
if node.parent_node is None:
|
||||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
|
@ -80,13 +73,8 @@ def setup_platform(hass, config: ConfigType,
|
|||
device = ISYBinarySensorDevice(node)
|
||||
devices.append(device)
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYBinarySensorProgram(program.name, status))
|
||||
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYBinarySensorProgram(name, status))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
@ -111,7 +99,7 @@ def _is_val_unknown(val):
|
|||
return val == -1*float('inf')
|
||||
|
||||
|
||||
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
||||
class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor device.
|
||||
|
||||
Often times, a single device is represented by multiple nodes in the ISY,
|
||||
|
@ -251,7 +239,7 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
|
|||
return self._device_class_from_type
|
||||
|
||||
|
||||
class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
|
||||
class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of the battery state of an ISY994 sensor."""
|
||||
|
||||
def __init__(self, node, parent_device) -> None:
|
||||
|
@ -354,7 +342,7 @@ class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
|
|||
return attr
|
||||
|
||||
|
||||
class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
|
||||
class ISYBinarySensorProgram(ISYDevice, BinarySensorDevice):
|
||||
"""Representation of an ISY994 binary sensor program.
|
||||
|
||||
This does not need all of the subnode logic in the device version of binary
|
||||
|
|
|
@ -8,8 +8,10 @@ import logging
|
|||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.components.cover import CoverDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -17,44 +19,32 @@ _LOGGER = logging.getLogger(__name__)
|
|||
VALUE_TO_STATE = {
|
||||
0: STATE_CLOSED,
|
||||
101: STATE_UNKNOWN,
|
||||
102: 'stopped',
|
||||
103: STATE_CLOSING,
|
||||
104: STATE_OPENING
|
||||
}
|
||||
|
||||
UOM = ['97']
|
||||
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 cover platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
devices.append(ISYCoverDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYCoverProgram(program.name, status, actions))
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYCoverProgram(name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYCoverDevice(isy.ISYDevice, CoverDevice):
|
||||
class ISYCoverDevice(ISYDevice, CoverDevice):
|
||||
"""Representation of an ISY994 cover device."""
|
||||
|
||||
def __init__(self, node: object):
|
||||
"""Initialize the ISY994 cover device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
|
@ -90,7 +80,7 @@ class ISYCoverProgram(ISYCoverDevice):
|
|||
|
||||
def __init__(self, name: str, node: object, actions: object) -> None:
|
||||
"""Initialize the ISY994 cover program."""
|
||||
ISYCoverDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
|
||||
|
|
|
@ -9,18 +9,13 @@ from typing import Callable
|
|||
|
||||
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
|
||||
SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
SPEED_HIGH, SUPPORT_SET_SPEED)
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Define term used for medium speed. This must be set as the fan component uses
|
||||
# 'medium' which the ISY does not understand
|
||||
ISY_SPEED_MEDIUM = 'med'
|
||||
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
0: SPEED_OFF,
|
||||
63: SPEED_LOW,
|
||||
|
@ -34,41 +29,28 @@ STATE_TO_VALUE = {}
|
|||
for key in VALUE_TO_STATE:
|
||||
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
|
||||
|
||||
STATES = [SPEED_OFF, SPEED_LOW, ISY_SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 fan platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
devices.append(ISYFanDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYFanProgram(program.name, status, actions))
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYFanProgram(name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYFanDevice(isy.ISYDevice, FanEntity):
|
||||
class ISYFanDevice(ISYDevice, FanEntity):
|
||||
"""Representation of an ISY994 fan device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 fan device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
|
@ -76,7 +58,7 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
|
|||
return VALUE_TO_STATE.get(self.value)
|
||||
|
||||
@property
|
||||
def is_on(self) -> str:
|
||||
def is_on(self) -> bool:
|
||||
"""Get if the fan is on."""
|
||||
return self.value != 0
|
||||
|
||||
|
@ -97,32 +79,32 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
|
|||
"""Get the list of available speeds."""
|
||||
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_SET_SPEED
|
||||
|
||||
|
||||
class ISYFanProgram(ISYFanDevice):
|
||||
"""Representation of an ISY994 fan program."""
|
||||
|
||||
def __init__(self, name: str, node, actions) -> None:
|
||||
"""Initialize the ISY994 fan program."""
|
||||
ISYFanDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 fan program."""
|
||||
return STATE_ON if bool(self.value) else STATE_OFF
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn on command to ISY994 fan program."""
|
||||
if not self._actions.runThen():
|
||||
_LOGGER.error("Unable to turn off the fan")
|
||||
else:
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Send the turn off command to ISY994 fan program."""
|
||||
if not self._actions.runElse():
|
||||
_LOGGER.error("Unable to turn on the fan")
|
||||
else:
|
||||
self.speed = STATE_ON if self.is_on else STATE_OFF
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return 0
|
||||
|
|
|
@ -24,15 +24,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DOMAIN = 'isy994'
|
||||
|
||||
CONF_HIDDEN_STRING = 'hidden_string'
|
||||
CONF_IGNORE_STRING = 'ignore_string'
|
||||
CONF_SENSOR_STRING = 'sensor_string'
|
||||
CONF_ENABLE_CLIMATE = 'enable_climate'
|
||||
CONF_TLS_VER = 'tls'
|
||||
|
||||
DEFAULT_HIDDEN_STRING = '{HIDE ME}'
|
||||
DEFAULT_IGNORE_STRING = '{IGNORE ME}'
|
||||
DEFAULT_SENSOR_STRING = 'sensor'
|
||||
|
||||
ISY = None
|
||||
|
||||
KEY_ACTIONS = 'actions'
|
||||
KEY_FOLDER = 'folder'
|
||||
KEY_MY_PROGRAMS = 'My Programs'
|
||||
|
@ -44,190 +43,344 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TLS_VER): vol.Coerce(float),
|
||||
vol.Optional(CONF_HIDDEN_STRING,
|
||||
default=DEFAULT_HIDDEN_STRING): cv.string,
|
||||
vol.Optional(CONF_IGNORE_STRING,
|
||||
default=DEFAULT_IGNORE_STRING): cv.string,
|
||||
vol.Optional(CONF_SENSOR_STRING,
|
||||
default=DEFAULT_SENSOR_STRING): cv.string
|
||||
default=DEFAULT_SENSOR_STRING): cv.string,
|
||||
vol.Optional(CONF_ENABLE_CLIMATE,
|
||||
default=True): cv.boolean
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SENSOR_NODES = []
|
||||
WEATHER_NODES = []
|
||||
NODES = []
|
||||
GROUPS = []
|
||||
PROGRAMS = {}
|
||||
# Do not use the Hass consts for the states here - we're matching exact API
|
||||
# responses, not using them for Hass states
|
||||
NODE_FILTERS = {
|
||||
'binary_sensor': {
|
||||
'uom': [],
|
||||
'states': [],
|
||||
'node_def_id': ['BinaryAlarm'],
|
||||
'insteon_type': ['16.'] # Does a startswith() match; include the dot
|
||||
},
|
||||
'sensor': {
|
||||
# This is just a more-readable way of including MOST uoms between 1-100
|
||||
# (Remember that range() is non-inclusive of the stop value)
|
||||
'uom': (['1'] +
|
||||
list(map(str, range(3, 11))) +
|
||||
list(map(str, range(12, 51))) +
|
||||
list(map(str, range(52, 66))) +
|
||||
list(map(str, range(69, 78))) +
|
||||
['79'] +
|
||||
list(map(str, range(82, 97)))),
|
||||
'states': [],
|
||||
'node_def_id': ['IMETER_SOLO'],
|
||||
'insteon_type': ['9.0.', '9.7.']
|
||||
},
|
||||
'lock': {
|
||||
'uom': ['11'],
|
||||
'states': ['locked', 'unlocked'],
|
||||
'node_def_id': ['DoorLock'],
|
||||
'insteon_type': ['15.']
|
||||
},
|
||||
'fan': {
|
||||
'uom': [],
|
||||
'states': ['on', 'off', 'low', 'medium', 'high'],
|
||||
'node_def_id': ['FanLincMotor'],
|
||||
'insteon_type': ['1.46.']
|
||||
},
|
||||
'cover': {
|
||||
'uom': ['97'],
|
||||
'states': ['open', 'closed', 'closing', 'opening', 'stopped'],
|
||||
'node_def_id': [],
|
||||
'insteon_type': []
|
||||
},
|
||||
'light': {
|
||||
'uom': ['51'],
|
||||
'states': ['on', 'off', '%'],
|
||||
'node_def_id': ['DimmerLampSwitch', 'DimmerLampSwitch_ADV',
|
||||
'DimmerSwitchOnly', 'DimmerSwitchOnly_ADV',
|
||||
'DimmerLampOnly', 'BallastRelayLampSwitch',
|
||||
'BallastRelayLampSwitch_ADV', 'RelayLampSwitch',
|
||||
'RemoteLinc2', 'RemoteLinc2_ADV'],
|
||||
'insteon_type': ['1.']
|
||||
},
|
||||
'switch': {
|
||||
'uom': ['2', '78'],
|
||||
'states': ['on', 'off'],
|
||||
'node_def_id': ['OnOffControl', 'RelayLampSwitch',
|
||||
'RelayLampSwitch_ADV', 'RelaySwitchOnlyPlusQuery',
|
||||
'RelaySwitchOnlyPlusQuery_ADV', 'RelayLampOnly',
|
||||
'RelayLampOnly_ADV', 'KeypadButton',
|
||||
'KeypadButton_ADV', 'EZRAIN_Input', 'EZRAIN_Output',
|
||||
'EZIO2x4_Input', 'EZIO2x4_Input_ADV', 'BinaryControl',
|
||||
'BinaryControl_ADV', 'AlertModuleSiren',
|
||||
'AlertModuleSiren_ADV', 'AlertModuleArmed', 'Siren',
|
||||
'Siren_ADV'],
|
||||
'insteon_type': ['2.', '9.10.', '9.11.']
|
||||
}
|
||||
}
|
||||
|
||||
PYISY = None
|
||||
SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover',
|
||||
'light', 'switch']
|
||||
SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch']
|
||||
|
||||
HIDDEN_STRING = DEFAULT_HIDDEN_STRING
|
||||
|
||||
SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock',
|
||||
'sensor', 'switch']
|
||||
# ISY Scenes are more like Swithes than Hass Scenes
|
||||
# (they can turn off, and report their state)
|
||||
SCENE_DOMAIN = 'switch'
|
||||
|
||||
ISY994_NODES = "isy994_nodes"
|
||||
ISY994_WEATHER = "isy994_weather"
|
||||
ISY994_PROGRAMS = "isy994_programs"
|
||||
|
||||
WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom'))
|
||||
|
||||
|
||||
def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
|
||||
"""Filter a list of ISY nodes based on the units and states provided."""
|
||||
filtered_nodes = []
|
||||
units = units if units else []
|
||||
states = states if states else []
|
||||
for node in nodes:
|
||||
match_unit = False
|
||||
match_state = True
|
||||
for uom in node.uom:
|
||||
if uom in units:
|
||||
match_unit = True
|
||||
continue
|
||||
elif uom not in states:
|
||||
match_state = False
|
||||
def _check_for_node_def(hass: HomeAssistant, node,
|
||||
single_domain: str=None) -> bool:
|
||||
"""Check if the node matches the node_def_id for any domains.
|
||||
|
||||
if match_unit:
|
||||
continue
|
||||
|
||||
if match_unit or match_state:
|
||||
filtered_nodes.append(node)
|
||||
|
||||
return filtered_nodes
|
||||
|
||||
|
||||
def _is_node_a_sensor(node, path: str, sensor_identifier: str) -> bool:
|
||||
"""Determine if the given node is a sensor."""
|
||||
if not isinstance(node, PYISY.Nodes.Node):
|
||||
This is only present on the 5.0 ISY firmware, and is the most reliable
|
||||
way to determine a device's type.
|
||||
"""
|
||||
if not hasattr(node, 'node_def_id') or node.node_def_id is None:
|
||||
# Node doesn't have a node_def (pre 5.0 firmware most likely)
|
||||
return False
|
||||
|
||||
if sensor_identifier in path or sensor_identifier in node.name:
|
||||
return True
|
||||
node_def_id = node.node_def_id
|
||||
|
||||
# This method is most reliable but only works on 5.x firmware
|
||||
try:
|
||||
if node.node_def_id == 'BinaryAlarm':
|
||||
domains = SUPPORTED_DOMAINS if not single_domain else [single_domain]
|
||||
for domain in domains:
|
||||
if node_def_id in NODE_FILTERS[domain]['node_def_id']:
|
||||
hass.data[ISY994_NODES][domain].append(node)
|
||||
return True
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# This method works on all firmwares, but only for Insteon devices
|
||||
try:
|
||||
device_type = node.type
|
||||
except AttributeError:
|
||||
# Node has no type; most likely not an Insteon device
|
||||
pass
|
||||
else:
|
||||
split_type = device_type.split('.')
|
||||
return split_type[0] == '16' # 16 represents Insteon binary sensors
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
|
||||
"""Categorize the ISY994 nodes."""
|
||||
global SENSOR_NODES
|
||||
global NODES
|
||||
global GROUPS
|
||||
def _check_for_insteon_type(hass: HomeAssistant, node,
|
||||
single_domain: str=None) -> bool:
|
||||
"""Check if the node matches the Insteon type for any domains.
|
||||
|
||||
SENSOR_NODES = []
|
||||
NODES = []
|
||||
GROUPS = []
|
||||
This is for (presumably) every version of the ISY firmware, but only
|
||||
works for Insteon device. "Node Server" (v5+) and Z-Wave and others will
|
||||
not have a type.
|
||||
"""
|
||||
if not hasattr(node, 'type') or node.type is None:
|
||||
# Node doesn't have a type (non-Insteon device most likely)
|
||||
return False
|
||||
|
||||
device_type = node.type
|
||||
domains = SUPPORTED_DOMAINS if not single_domain else [single_domain]
|
||||
for domain in domains:
|
||||
if any([device_type.startswith(t) for t in
|
||||
set(NODE_FILTERS[domain]['insteon_type'])]):
|
||||
hass.data[ISY994_NODES][domain].append(node)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _check_for_uom_id(hass: HomeAssistant, node,
|
||||
single_domain: str=None, uom_list: list=None) -> bool:
|
||||
"""Check if a node's uom matches any of the domains uom filter.
|
||||
|
||||
This is used for versions of the ISY firmware that report uoms as a single
|
||||
ID. We can often infer what type of device it is by that ID.
|
||||
"""
|
||||
if not hasattr(node, 'uom') or node.uom is None:
|
||||
# Node doesn't have a uom (Scenes for example)
|
||||
return False
|
||||
|
||||
node_uom = set(map(str.lower, node.uom))
|
||||
|
||||
if uom_list:
|
||||
if node_uom.intersection(NODE_FILTERS[single_domain]['uom']):
|
||||
hass.data[ISY994_NODES][single_domain].append(node)
|
||||
return True
|
||||
else:
|
||||
domains = SUPPORTED_DOMAINS if not single_domain else [single_domain]
|
||||
for domain in domains:
|
||||
if node_uom.intersection(NODE_FILTERS[domain]['uom']):
|
||||
hass.data[ISY994_NODES][domain].append(node)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _check_for_states_in_uom(hass: HomeAssistant, node,
|
||||
single_domain: str=None,
|
||||
states_list: list=None) -> bool:
|
||||
"""Check if a list of uoms matches two possible filters.
|
||||
|
||||
This is for versions of the ISY firmware that report uoms as a list of all
|
||||
possible "human readable" states. This filter passes if all of the possible
|
||||
states fit inside the given filter.
|
||||
"""
|
||||
if not hasattr(node, 'uom') or node.uom is None:
|
||||
# Node doesn't have a uom (Scenes for example)
|
||||
return False
|
||||
|
||||
node_uom = set(map(str.lower, node.uom))
|
||||
|
||||
if states_list:
|
||||
if node_uom == set(states_list):
|
||||
hass.data[ISY994_NODES][single_domain].append(node)
|
||||
return True
|
||||
else:
|
||||
domains = SUPPORTED_DOMAINS if not single_domain else [single_domain]
|
||||
for domain in domains:
|
||||
if node_uom == set(NODE_FILTERS[domain]['states']):
|
||||
hass.data[ISY994_NODES][domain].append(node)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool:
|
||||
"""Determine if the given sensor node should be a binary_sensor."""
|
||||
if _check_for_node_def(hass, node, single_domain='binary_sensor'):
|
||||
return True
|
||||
if _check_for_insteon_type(hass, node, single_domain='binary_sensor'):
|
||||
return True
|
||||
|
||||
# For the next two checks, we're providing our own set of uoms that
|
||||
# represent on/off devices. This is because we can only depend on these
|
||||
# checks in the context of already knowing that this is definitely a
|
||||
# sensor device.
|
||||
if _check_for_uom_id(hass, node, single_domain='binary_sensor',
|
||||
uom_list=['2', '78']):
|
||||
return True
|
||||
if _check_for_states_in_uom(hass, node, single_domain='binary_sensor',
|
||||
states_list=['on', 'off']):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str,
|
||||
sensor_identifier: str)-> None:
|
||||
"""Sort the nodes to their proper domains."""
|
||||
# pylint: disable=no-member
|
||||
for (path, node) in ISY.nodes:
|
||||
hidden = hidden_identifier in path or hidden_identifier in node.name
|
||||
if hidden:
|
||||
node.name += hidden_identifier
|
||||
if _is_node_a_sensor(node, path, sensor_identifier):
|
||||
SENSOR_NODES.append(node)
|
||||
elif isinstance(node, PYISY.Nodes.Node):
|
||||
NODES.append(node)
|
||||
elif isinstance(node, PYISY.Nodes.Group):
|
||||
GROUPS.append(node)
|
||||
for (path, node) in nodes:
|
||||
ignored = ignore_identifier in path or ignore_identifier in node.name
|
||||
if ignored:
|
||||
# Don't import this node as a device at all
|
||||
continue
|
||||
|
||||
from PyISY.Nodes import Group
|
||||
if isinstance(node, Group):
|
||||
hass.data[ISY994_NODES][SCENE_DOMAIN].append(node)
|
||||
continue
|
||||
|
||||
if sensor_identifier in path or sensor_identifier in node.name:
|
||||
# User has specified to treat this as a sensor. First we need to
|
||||
# determine if it should be a binary_sensor.
|
||||
if _is_sensor_a_binary_sensor(hass, node):
|
||||
continue
|
||||
else:
|
||||
hass.data[ISY994_NODES]['sensor'].append(node)
|
||||
continue
|
||||
|
||||
# We have a bunch of different methods for determining the device type,
|
||||
# each of which works with different ISY firmware versions or device
|
||||
# family. The order here is important, from most reliable to least.
|
||||
if _check_for_node_def(hass, node):
|
||||
continue
|
||||
if _check_for_insteon_type(hass, node):
|
||||
continue
|
||||
if _check_for_uom_id(hass, node):
|
||||
continue
|
||||
if _check_for_states_in_uom(hass, node):
|
||||
continue
|
||||
|
||||
|
||||
def _categorize_programs() -> None:
|
||||
def _categorize_programs(hass: HomeAssistant, programs: dict) -> None:
|
||||
"""Categorize the ISY994 programs."""
|
||||
global PROGRAMS
|
||||
|
||||
PROGRAMS = {}
|
||||
|
||||
for component in SUPPORTED_DOMAINS:
|
||||
for domain in SUPPORTED_PROGRAM_DOMAINS:
|
||||
try:
|
||||
folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component]
|
||||
folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
for dtype, _, node_id in folder.children:
|
||||
if dtype is KEY_FOLDER:
|
||||
program = folder[node_id]
|
||||
if dtype == KEY_FOLDER:
|
||||
entity_folder = folder[node_id]
|
||||
try:
|
||||
node = program[KEY_STATUS].leaf
|
||||
assert node.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
if component not in PROGRAMS:
|
||||
PROGRAMS[component] = []
|
||||
PROGRAMS[component].append(program)
|
||||
status = entity_folder[KEY_STATUS]
|
||||
assert status.dtype == 'program', 'Not a program'
|
||||
if domain != 'binary_sensor':
|
||||
actions = entity_folder[KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
else:
|
||||
actions = None
|
||||
except (AttributeError, KeyError, AssertionError):
|
||||
_LOGGER.warning("Program entity '%s' not loaded due "
|
||||
"to invalid folder structure.",
|
||||
entity_folder.name)
|
||||
continue
|
||||
|
||||
entity = (entity_folder.name, status, actions)
|
||||
hass.data[ISY994_PROGRAMS][domain].append(entity)
|
||||
|
||||
|
||||
def _categorize_weather() -> None:
|
||||
def _categorize_weather(hass: HomeAssistant, climate) -> None:
|
||||
"""Categorize the ISY994 weather data."""
|
||||
global WEATHER_NODES
|
||||
|
||||
climate_attrs = dir(ISY.climate)
|
||||
WEATHER_NODES = [WeatherNode(getattr(ISY.climate, attr), attr,
|
||||
getattr(ISY.climate, attr + '_units'))
|
||||
climate_attrs = dir(climate)
|
||||
weather_nodes = [WeatherNode(getattr(climate, attr),
|
||||
attr.replace('_', ' '),
|
||||
getattr(climate, '{}_units'.format(attr)))
|
||||
for attr in climate_attrs
|
||||
if attr + '_units' in climate_attrs]
|
||||
if '{}_units'.format(attr) in climate_attrs]
|
||||
hass.data[ISY994_WEATHER].extend(weather_nodes)
|
||||
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ISY 994 platform."""
|
||||
hass.data[ISY994_NODES] = {}
|
||||
for domain in SUPPORTED_DOMAINS:
|
||||
hass.data[ISY994_NODES][domain] = []
|
||||
|
||||
hass.data[ISY994_WEATHER] = []
|
||||
|
||||
hass.data[ISY994_PROGRAMS] = {}
|
||||
for domain in SUPPORTED_DOMAINS:
|
||||
hass.data[ISY994_PROGRAMS][domain] = []
|
||||
|
||||
isy_config = config.get(DOMAIN)
|
||||
|
||||
user = isy_config.get(CONF_USERNAME)
|
||||
password = isy_config.get(CONF_PASSWORD)
|
||||
tls_version = isy_config.get(CONF_TLS_VER)
|
||||
host = urlparse(isy_config.get(CONF_HOST))
|
||||
port = host.port
|
||||
addr = host.geturl()
|
||||
hidden_identifier = isy_config.get(
|
||||
CONF_HIDDEN_STRING, DEFAULT_HIDDEN_STRING)
|
||||
sensor_identifier = isy_config.get(
|
||||
CONF_SENSOR_STRING, DEFAULT_SENSOR_STRING)
|
||||
|
||||
global HIDDEN_STRING
|
||||
HIDDEN_STRING = hidden_identifier
|
||||
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':
|
||||
addr = addr.replace('http://', '')
|
||||
https = False
|
||||
port = host.port or 80
|
||||
elif host.scheme == 'https':
|
||||
addr = addr.replace('https://', '')
|
||||
https = True
|
||||
port = host.port or 443
|
||||
else:
|
||||
_LOGGER.error("isy994 host value in configuration is invalid")
|
||||
return False
|
||||
|
||||
addr = addr.replace(':{}'.format(port), '')
|
||||
|
||||
import PyISY
|
||||
|
||||
global PYISY
|
||||
PYISY = PyISY
|
||||
|
||||
# Connect to ISY controller.
|
||||
global ISY
|
||||
ISY = PyISY.ISY(addr, port, username=user, password=password,
|
||||
isy = PyISY.ISY(host.hostname, port, username=user, password=password,
|
||||
use_https=https, tls_ver=tls_version, log=_LOGGER)
|
||||
if not ISY.connected:
|
||||
if not isy.connected:
|
||||
return False
|
||||
|
||||
_categorize_nodes(hidden_identifier, sensor_identifier)
|
||||
_categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier)
|
||||
_categorize_programs(hass, isy.programs)
|
||||
|
||||
_categorize_programs()
|
||||
if enable_climate and isy.configuration.get('Weather Information'):
|
||||
_categorize_weather(hass, isy.climate)
|
||||
|
||||
if ISY.configuration.get('Weather Information'):
|
||||
_categorize_weather()
|
||||
def stop(event: object) -> None:
|
||||
"""Stop ISY auto updates."""
|
||||
isy.auto_update = False
|
||||
|
||||
# Listen for HA stop to disconnect.
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
|
||||
|
@ -236,21 +389,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
for component in SUPPORTED_DOMAINS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
ISY.auto_update = True
|
||||
isy.auto_update = True
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def stop(event: object) -> None:
|
||||
"""Stop ISY auto updates."""
|
||||
ISY.auto_update = False
|
||||
|
||||
|
||||
class ISYDevice(Entity):
|
||||
"""Representation of an ISY994 device."""
|
||||
|
||||
_attrs = {}
|
||||
_domain = None # type: str
|
||||
_name = None # type: str
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
|
@ -281,28 +427,16 @@ class ISYDevice(Entity):
|
|||
'control': event
|
||||
})
|
||||
|
||||
@property
|
||||
def domain(self) -> str:
|
||||
"""Get the domain of the device."""
|
||||
return self._domain
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Get the unique identifier of the device."""
|
||||
# pylint: disable=protected-access
|
||||
return self._node._id
|
||||
|
||||
@property
|
||||
def raw_name(self) -> str:
|
||||
"""Get the raw name of the device."""
|
||||
return str(self._name) \
|
||||
if self._name is not None else str(self._node.name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the name of the device."""
|
||||
return self.raw_name.replace(HIDDEN_STRING, '').strip() \
|
||||
.replace('_', ' ')
|
||||
return self._name or str(self._node.name)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -310,7 +444,7 @@ class ISYDevice(Entity):
|
|||
return False
|
||||
|
||||
@property
|
||||
def value(self) -> object:
|
||||
def value(self) -> int:
|
||||
"""Get the current value of the device."""
|
||||
# pylint: disable=protected-access
|
||||
return self._node.status._val
|
||||
|
@ -338,22 +472,3 @@ class ISYDevice(Entity):
|
|||
for name, val in self._node.aux_properties.items():
|
||||
attr[name] = '{} {}'.format(val.get('value'), val.get('uom'))
|
||||
return attr
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
"""Get whether the device should be hidden from the UI."""
|
||||
return HIDDEN_STRING in self.raw_name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Get the device unit of measure."""
|
||||
return None
|
||||
|
||||
def _attr_filter(self, attr: str) -> str:
|
||||
"""Filter the attribute."""
|
||||
# pylint: disable=no-self-use
|
||||
return attr
|
||||
|
||||
def update(self) -> None:
|
||||
"""Perform an update for the device."""
|
||||
pass
|
||||
|
|
|
@ -8,40 +8,30 @@ import logging
|
|||
from typing import Callable
|
||||
|
||||
from homeassistant.components.light import (
|
||||
Light, SUPPORT_BRIGHTNESS)
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
Light, SUPPORT_BRIGHTNESS, DOMAIN)
|
||||
from homeassistant.components.isy994 import ISY994_NODES, ISYDevice
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UOM = ['2', '51', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false', '%']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 light platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES):
|
||||
if node.dimmable or '51' in node.uom:
|
||||
devices.append(ISYLightDevice(node))
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
devices.append(ISYLightDevice(node))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYLightDevice(isy.ISYDevice, Light):
|
||||
class ISYLightDevice(ISYDevice, Light):
|
||||
"""Representation of an ISY994 light devie."""
|
||||
|
||||
def __init__(self, node: object) -> None:
|
||||
"""Initialize the ISY994 light device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
|
|
@ -8,7 +8,8 @@ import logging
|
|||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.components.lock import LockDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
|
@ -19,43 +20,27 @@ VALUE_TO_STATE = {
|
|||
100: STATE_LOCKED
|
||||
}
|
||||
|
||||
UOM = ['11']
|
||||
STATES = [STATE_LOCKED, STATE_UNLOCKED]
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 lock platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM,
|
||||
states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
devices.append(ISYLockDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYLockProgram(program.name, status, actions))
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYLockProgram(name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYLockDevice(isy.ISYDevice, LockDevice):
|
||||
class ISYLockDevice(ISYDevice, LockDevice):
|
||||
"""Representation of an ISY994 lock device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 lock device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._conn = node.parent.parent.conn
|
||||
|
||||
@property
|
||||
|
@ -101,7 +86,7 @@ class ISYLockProgram(ISYLockDevice):
|
|||
|
||||
def __init__(self, name: str, node, actions) -> None:
|
||||
"""Initialize the lock."""
|
||||
ISYLockDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
|
||||
|
|
|
@ -7,9 +7,11 @@ https://home-assistant.io/components/sensor.isy994/
|
|||
import logging
|
||||
from typing import Callable # noqa
|
||||
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER,
|
||||
ISYDevice)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF, STATE_ON, UNIT_UV_INDEX)
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -232,37 +234,29 @@ UOM_TO_STATES = {
|
|||
}
|
||||
}
|
||||
|
||||
BINARY_UOM = ['2', '78']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 sensor platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error("A connection has not been made to the ISY controller")
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.SENSOR_NODES:
|
||||
if (not node.uom or node.uom[0] not in BINARY_UOM) and \
|
||||
STATE_OFF not in node.uom and STATE_ON not in node.uom:
|
||||
_LOGGER.debug("Loading %s", node.name)
|
||||
devices.append(ISYSensorDevice(node))
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
_LOGGER.debug("Loading %s", node.name)
|
||||
devices.append(ISYSensorDevice(node))
|
||||
|
||||
for node in isy.WEATHER_NODES:
|
||||
for node in hass.data[ISY994_WEATHER]:
|
||||
devices.append(ISYWeatherDevice(node))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYSensorDevice(isy.ISYDevice):
|
||||
class ISYSensorDevice(ISYDevice):
|
||||
"""Representation of an ISY994 sensor device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 sensor device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def raw_unit_of_measurement(self) -> str:
|
||||
|
@ -316,14 +310,12 @@ class ISYSensorDevice(isy.ISYDevice):
|
|||
return raw_units
|
||||
|
||||
|
||||
class ISYWeatherDevice(isy.ISYDevice):
|
||||
class ISYWeatherDevice(ISYDevice):
|
||||
"""Representation of an ISY994 weather device."""
|
||||
|
||||
_domain = 'sensor'
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 weather device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
|
|
|
@ -8,71 +8,39 @@ import logging
|
|||
from typing import Callable # noqa
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice, DOMAIN
|
||||
import homeassistant.components.isy994 as isy
|
||||
from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN
|
||||
from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS,
|
||||
ISYDevice)
|
||||
from homeassistant.helpers.typing import ConfigType # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VALUE_TO_STATE = {
|
||||
False: STATE_OFF,
|
||||
True: STATE_ON,
|
||||
}
|
||||
|
||||
UOM = ['2', '78']
|
||||
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 switch platform."""
|
||||
if isy.ISY is None or not isy.ISY.connected:
|
||||
_LOGGER.error('A connection has not been made to the ISY controller.')
|
||||
return False
|
||||
|
||||
devices = []
|
||||
|
||||
for node in isy.filter_nodes(isy.NODES, units=UOM,
|
||||
states=STATES):
|
||||
for node in hass.data[ISY994_NODES][DOMAIN]:
|
||||
if not node.dimmable:
|
||||
devices.append(ISYSwitchDevice(node))
|
||||
|
||||
for node in isy.GROUPS:
|
||||
devices.append(ISYSwitchDevice(node))
|
||||
|
||||
for program in isy.PROGRAMS.get(DOMAIN, []):
|
||||
try:
|
||||
status = program[isy.KEY_STATUS]
|
||||
actions = program[isy.KEY_ACTIONS]
|
||||
assert actions.dtype == 'program', 'Not a program'
|
||||
except (KeyError, AssertionError):
|
||||
pass
|
||||
else:
|
||||
devices.append(ISYSwitchProgram(program.name, status, actions))
|
||||
for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYSwitchProgram(name, status, actions))
|
||||
|
||||
add_devices(devices)
|
||||
|
||||
|
||||
class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
|
||||
class ISYSwitchDevice(ISYDevice, SwitchDevice):
|
||||
"""Representation of an ISY994 switch device."""
|
||||
|
||||
def __init__(self, node) -> None:
|
||||
"""Initialize the ISY994 switch device."""
|
||||
isy.ISYDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get whether the ISY994 device is in the on state."""
|
||||
return self.state == STATE_ON
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 device."""
|
||||
if self.is_unknown():
|
||||
return None
|
||||
else:
|
||||
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
|
||||
return bool(self.value)
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Send the turn on command to the ISY994 switch."""
|
||||
|
@ -90,7 +58,7 @@ class ISYSwitchProgram(ISYSwitchDevice):
|
|||
|
||||
def __init__(self, name: str, node, actions) -> None:
|
||||
"""Initialize the ISY994 switch program."""
|
||||
ISYSwitchDevice.__init__(self, node)
|
||||
super().__init__(node)
|
||||
self._name = name
|
||||
self._actions = actions
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue