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:
Greg Laabs 2017-12-26 00:26:37 -08:00 committed by Pascal Vizeli
parent a59b02b6b4
commit d687bc073e
8 changed files with 358 additions and 348 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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