From 172e7c1d38a3dd249f5ecd836be11fd6192e2b1f Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 19 Dec 2017 16:15:10 -0800 Subject: [PATCH 01/10] 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 --- .../components/binary_sensor/isy994.py | 28 +- homeassistant/components/cover/isy994.py | 35 +- homeassistant/components/fan/isy994.py | 57 ++- homeassistant/components/isy994.py | 408 +++++++++++------- homeassistant/components/light/isy994.py | 22 +- homeassistant/components/lock/isy994.py | 30 +- homeassistant/components/sensor/isy994.py | 22 +- homeassistant/components/switch/isy994.py | 49 +-- 8 files changed, 347 insertions(+), 304 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a5b61c9ffedee5..e1171599da29b4 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -12,7 +12,8 @@ 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, + KEY_STATUS, 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 @@ _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 @@ 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,11 +73,12 @@ def setup_platform(hass, config: ConfigType, device = ISYBinarySensorDevice(node) devices.append(device) - for program in isy.PROGRAMS.get(DOMAIN, []): + for program in hass.data[ISY994_PROGRAMS][DOMAIN]: try: - status = program[isy.KEY_STATUS] - except (KeyError, AssertionError): - pass + status = program[KEY_STATUS] + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due to" + "incompatible folder structure.", program.name) else: devices.append(ISYBinarySensorProgram(program.name, status)) @@ -111,7 +105,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, @@ -250,7 +244,7 @@ def device_class(self) -> str: 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: @@ -353,7 +347,7 @@ def device_state_attributes(self): 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 diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 4dd1c9be364a11..cc8ddcf445102a 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -8,8 +8,11 @@ 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, + KEY_STATUS, KEY_ACTIONS, + 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 +20,40 @@ 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, []): + for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] + status = program[KEY_STATUS] + actions = program[KEY_ACTIONS] assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due to" + "incompatible folder structure.", program.name) else: devices.append(ISYCoverProgram(program.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) + ISYDevice.__init__(self, node) @property def current_cover_position(self) -> int: diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index a49952569a8742..1b3d9412d74f37 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -9,18 +9,14 @@ 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, + KEY_STATUS, KEY_ACTIONS, + 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 +30,36 @@ 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, []): + for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] + status = program[KEY_STATUS] + actions = program[KEY_ACTIONS] assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due to" + "incompatible folder structure.", program.name) else: devices.append(ISYFanProgram(program.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) + ISYDevice.__init__(self, node) @property def speed(self) -> str: @@ -76,7 +67,7 @@ def speed(self) -> str: 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,6 +88,11 @@ def speed_list(self) -> list: """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.""" @@ -106,23 +102,18 @@ def __init__(self, name: str, node, actions) -> None: ISYFanDevice.__init__(self, 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 diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index af1846c7bf8173..abbcb8f06e315b 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -24,15 +24,13 @@ DOMAIN = 'isy994' -CONF_HIDDEN_STRING = 'hidden_string' +CONF_IGNORE_STRING = 'ignore_string' CONF_SENSOR_STRING = 'sensor_string' 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,114 +42,259 @@ 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 }) }, 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.'] + } +} + +SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', + 'light', '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" -PYISY = None +WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) -HIDDEN_STRING = DEFAULT_HIDDEN_STRING -SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock', - 'sensor', 'switch'] +def _check_for_node_def(node, hass: HomeAssistant, + single_domain: str=None) -> bool: + """Check if the node matches the node_def_id for any domains. + This is only present on the 5.0 ISY firmware, and is the most reliable + way to determine a device's type. + """ + try: + node_def_id = node.node_def_id + except AttributeError: + # Node doesn't have a node_def + return False -WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) + 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 + return False -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 - if match_unit: - continue +def _check_for_insteon_type(node, hass: HomeAssistant, + single_domain: str=None) -> bool: + """Check if the node matches the Insteon type for any domains. - if match_unit or match_state: - filtered_nodes.append(node) + 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. + """ + try: + device_type = node.type + except AttributeError: + # Node doesn't have a type (non-Insteon device most likely) + return False - return filtered_nodes + 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 _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): - return False - if sensor_identifier in path or sensor_identifier in node.name: - return True +def _check_for_uom_id(node, hass: HomeAssistant, + single_domain: str=None, uom_list: list=None) -> bool: + """Check if a node's uom matches any of the domains uom filter. - # This method is most reliable but only works on 5.x firmware + 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. + """ try: - if node.node_def_id == 'BinaryAlarm': - return True + node_uom = set(node.uom) except AttributeError: - pass + # Node doesn't have a uom (Scenes for example) + return False + + 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 - # This method works on all firmwares, but only for Insteon devices + +def _check_for_states_in_uom(node, hass: HomeAssistant, + 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. + """ try: - device_type = node.type + node_uom = set(map(str.lower, node.uom)) except AttributeError: - # Node has no type; most likely not an Insteon device - pass + # Node doesn't have a uom + return False + + if states_list: + if node_uom == set(states_list): + hass.data[ISY994_NODES][single_domain].append(node) + return True else: - split_type = device_type.split('.') - return split_type[0] == '16' # 16 represents Insteon binary sensors + 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 _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None: - """Categorize the ISY994 nodes.""" - global SENSOR_NODES - global NODES - global GROUPS +def _is_sensor_a_binary_sensor(node, hass: HomeAssistant) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(node, hass, single_domain='binary_sensor'): + return True + if _check_for_insteon_type(node, hass, single_domain='binary_sensor'): + return True - SENSOR_NODES = [] - NODES = [] - GROUPS = [] + # 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(node, hass, single_domain='binary_sensor', + uom_list=['2', '78']): + return True + if _check_for_states_in_uom(node, hass, single_domain='binary_sensor', + states_list=['on', 'off']): + return True + + return False + +def _categorize_nodes(nodes, hass: HomeAssistant, 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) - - -def _categorize_programs() -> None: - """Categorize the ISY994 programs.""" - global PROGRAMS + 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(node, hass): + continue + else: + hass.data[ISY994_NODES]['sensor'].append(node) + continue - PROGRAMS = {} + # 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(node, hass): + continue + if _check_for_insteon_type(node, hass): + continue + if _check_for_uom_id(node, hass): + continue + if _check_for_states_in_uom(node, hass): + continue - for component in SUPPORTED_DOMAINS: + +def _categorize_programs(programs: dict, hass: HomeAssistant) -> None: + """Categorize the ISY994 programs.""" + for domain in SUPPORTED_DOMAINS: try: - folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component] + folder = programs[KEY_MY_PROGRAMS]['HA.' + domain] except KeyError: pass else: @@ -164,70 +307,71 @@ def _categorize_programs() -> None: except (KeyError, AssertionError): pass else: - if component not in PROGRAMS: - PROGRAMS[component] = [] - PROGRAMS[component].append(program) + hass.data[ISY994_PROGRAMS][domain].append(program) -def _categorize_weather() -> None: +def _categorize_weather(climate, hass: HomeAssistant) -> 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, + getattr(climate, attr + '_units')) for attr in climate_attrs if attr + '_units' in climate_attrs] + hass.data[ISY994_WEATHER].append(weather_nodes) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ISY 994 platform.""" + if ISY994_NODES not in hass.data: + hass.data[ISY994_NODES] = {} + for domain in SUPPORTED_DOMAINS: + if domain not in hass.data[ISY994_NODES]: + hass.data[ISY994_NODES][domain] = [] + + if ISY994_WEATHER not in hass.data: + hass.data[ISY994_WEATHER] = {} + + if ISY994_PROGRAMS not in hass.data: + hass.data[ISY994_PROGRAMS] = {} + for domain in SUPPORTED_DOMAINS: + if domain not in hass.data[ISY994_PROGRAMS]: + 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) 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(isy.nodes, hass, ignore_identifier, sensor_identifier) + _categorize_programs(isy.programs, hass) - _categorize_programs() + if isy.configuration.get('Weather Information'): + _categorize_weather(isy.climate, hass) - 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 +380,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 +418,16 @@ def on_control(self, event: object) -> None: '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 +435,7 @@ def should_poll(self) -> bool: 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 +463,3 @@ def device_state_attributes(self) -> Dict: 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 diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 78b92fbd1453dd..5ad08aa061c55a 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -8,40 +8,30 @@ 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) + ISYDevice.__init__(self, node) @property def is_on(self) -> bool: diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 63272b90b1fa80..527716a9d066bf 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -8,7 +8,9 @@ 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, + KEY_ACTIONS, KEY_STATUS, + ISYDevice) from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN from homeassistant.helpers.typing import ConfigType @@ -19,43 +21,35 @@ 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, []): + for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] + status = program[KEY_STATUS] + actions = program[KEY_ACTIONS] assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program entity '%s' not loaded due to" + "incompatible folder structure.", program.name) else: devices.append(ISYLockProgram(program.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) + ISYDevice.__init__(self, node) self._conn = node.parent.parent.conn @property diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index e961c63a1b511c..7a37d7f1e1114d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -7,9 +7,11 @@ import logging from typing import Callable # noqa +from homeassistant.components.sensor import DOMAIN import homeassistant.components.isy994 as isy +from homeassistant.components.isy994 import ISY994_NODES, ISY994_WEATHER 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,26 +234,18 @@ } } -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) @@ -319,8 +313,6 @@ def unit_of_measurement(self) -> str: class ISYWeatherDevice(isy.ISYDevice): """Representation of an ISY994 weather device.""" - _domain = 'sensor' - def __init__(self, node) -> None: """Initialize the ISY994 weather device.""" isy.ISYDevice.__init__(self, node) diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 0f1ec62eaeef04..4d1bdb254ab02d 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -8,71 +8,48 @@ 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, + KEY_ACTIONS, KEY_STATUS, + 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, []): + for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): try: - status = program[isy.KEY_STATUS] - actions = program[isy.KEY_ACTIONS] + status = program[KEY_STATUS] + actions = program[KEY_ACTIONS] assert actions.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning("Program '%s' failed to load due to " + "incompatible folder structure.", program.name) else: devices.append(ISYSwitchProgram(program.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) + ISYDevice.__init__(self, 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.""" From df10d7a67080facbc33ba2c482027e089388af00 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 19 Dec 2017 23:27:25 -0800 Subject: [PATCH 02/10] Use `super()` instead of explicit base class calls --- homeassistant/components/cover/isy994.py | 4 ++-- homeassistant/components/fan/isy994.py | 4 ++-- homeassistant/components/light/isy994.py | 2 +- homeassistant/components/lock/isy994.py | 4 ++-- homeassistant/components/sensor/isy994.py | 12 ++++++------ homeassistant/components/switch/isy994.py | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index cc8ddcf445102a..4bfd45e691671b 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -53,7 +53,7 @@ class ISYCoverDevice(ISYDevice, CoverDevice): def __init__(self, node: object): """Initialize the ISY994 cover device.""" - ISYDevice.__init__(self, node) + super().__init__(node) @property def current_cover_position(self) -> int: @@ -89,7 +89,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 diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index 1b3d9412d74f37..be6e53ba0da560 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -59,7 +59,7 @@ class ISYFanDevice(ISYDevice, FanEntity): def __init__(self, node) -> None: """Initialize the ISY994 fan device.""" - ISYDevice.__init__(self, node) + super().__init__(node) @property def speed(self) -> str: @@ -99,7 +99,7 @@ class ISYFanProgram(ISYFanDevice): 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 diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 5ad08aa061c55a..a6191b05c7cf00 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -31,7 +31,7 @@ class ISYLightDevice(ISYDevice, Light): def __init__(self, node: object) -> None: """Initialize the ISY994 light device.""" - ISYDevice.__init__(self, node) + super().__init__(node) @property def is_on(self) -> bool: diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 527716a9d066bf..6cdc269251e70d 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -49,7 +49,7 @@ class ISYLockDevice(ISYDevice, LockDevice): def __init__(self, node) -> None: """Initialize the ISY994 lock device.""" - ISYDevice.__init__(self, node) + super().__init__(node) self._conn = node.parent.parent.conn @property @@ -95,7 +95,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 diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 7a37d7f1e1114d..76f026bba10d3b 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -8,8 +8,8 @@ from typing import Callable # noqa from homeassistant.components.sensor import DOMAIN -import homeassistant.components.isy994 as isy -from homeassistant.components.isy994 import ISY994_NODES, ISY994_WEATHER +from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, + ISYDevice) from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX) from homeassistant.helpers.typing import ConfigType @@ -251,12 +251,12 @@ def setup_platform(hass, config: ConfigType, 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: @@ -310,12 +310,12 @@ def unit_of_measurement(self) -> str: return raw_units -class ISYWeatherDevice(isy.ISYDevice): +class ISYWeatherDevice(ISYDevice): """Representation of an ISY994 weather device.""" def __init__(self, node) -> None: """Initialize the ISY994 weather device.""" - isy.ISYDevice.__init__(self, node) + super().__init__(node) @property def unique_id(self) -> str: diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 4d1bdb254ab02d..240a994626f6b3 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -44,7 +44,7 @@ class ISYSwitchDevice(ISYDevice, SwitchDevice): def __init__(self, node) -> None: """Initialize the ISY994 switch device.""" - ISYDevice.__init__(self, node) + super().__init__(node) @property def is_on(self) -> bool: @@ -67,7 +67,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 From 742beb217a70bddcd66467880fa85521adbca46e Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 19 Dec 2017 23:32:38 -0800 Subject: [PATCH 03/10] Move `hass` argument to first position --- homeassistant/components/isy994.py | 40 +++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index abbcb8f06e315b..d773c492ec4188 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -130,7 +130,7 @@ WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom')) -def _check_for_node_def(node, hass: HomeAssistant, +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. @@ -152,7 +152,7 @@ def _check_for_node_def(node, hass: HomeAssistant, return False -def _check_for_insteon_type(node, hass: HomeAssistant, +def _check_for_insteon_type(hass: HomeAssistant, node, single_domain: str=None) -> bool: """Check if the node matches the Insteon type for any domains. @@ -176,7 +176,7 @@ def _check_for_insteon_type(node, hass: HomeAssistant, return False -def _check_for_uom_id(node, hass: HomeAssistant, +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. @@ -203,7 +203,7 @@ def _check_for_uom_id(node, hass: HomeAssistant, return False -def _check_for_states_in_uom(node, hass: HomeAssistant, +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. @@ -232,28 +232,28 @@ def _check_for_states_in_uom(node, hass: HomeAssistant, return False -def _is_sensor_a_binary_sensor(node, hass: HomeAssistant) -> bool: +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(node, hass, single_domain='binary_sensor'): + if _check_for_node_def(hass, node, single_domain='binary_sensor'): return True - if _check_for_insteon_type(node, hass, single_domain='binary_sensor'): + 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(node, hass, single_domain='binary_sensor', + if _check_for_uom_id(hass, node, single_domain='binary_sensor', uom_list=['2', '78']): return True - if _check_for_states_in_uom(node, hass, single_domain='binary_sensor', + if _check_for_states_in_uom(hass, node, single_domain='binary_sensor', states_list=['on', 'off']): return True return False -def _categorize_nodes(nodes, hass: HomeAssistant, ignore_identifier: str, +def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str)-> None: """Sort the nodes to their proper domains.""" # pylint: disable=no-member @@ -271,7 +271,7 @@ def _categorize_nodes(nodes, hass: HomeAssistant, ignore_identifier: str, 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(node, hass): + if _is_sensor_a_binary_sensor(hass, node): continue else: hass.data[ISY994_NODES]['sensor'].append(node) @@ -280,17 +280,17 @@ def _categorize_nodes(nodes, hass: HomeAssistant, ignore_identifier: str, # 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(node, hass): + if _check_for_node_def(hass, node): continue - if _check_for_insteon_type(node, hass): + if _check_for_insteon_type(hass, node): continue - if _check_for_uom_id(node, hass): + if _check_for_uom_id(hass, node): continue - if _check_for_states_in_uom(node, hass): + if _check_for_states_in_uom(hass, node): continue -def _categorize_programs(programs: dict, hass: HomeAssistant) -> None: +def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" for domain in SUPPORTED_DOMAINS: try: @@ -310,7 +310,7 @@ def _categorize_programs(programs: dict, hass: HomeAssistant) -> None: hass.data[ISY994_PROGRAMS][domain].append(program) -def _categorize_weather(climate, hass: HomeAssistant) -> None: +def _categorize_weather(hass: HomeAssistant, climate) -> None: """Categorize the ISY994 weather data.""" climate_attrs = dir(climate) weather_nodes = [WeatherNode(getattr(climate, attr), attr, @@ -363,11 +363,11 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: if not isy.connected: return False - _categorize_nodes(isy.nodes, hass, ignore_identifier, sensor_identifier) - _categorize_programs(isy.programs, hass) + _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(hass, isy.programs) if isy.configuration.get('Weather Information'): - _categorize_weather(isy.climate, hass) + _categorize_weather(hass, isy.climate) def stop(event: object) -> None: """Stop ISY auto updates.""" From eacbf1051d90d410ec01a93ef9843bf2f4e04006 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 19 Dec 2017 23:36:07 -0800 Subject: [PATCH 04/10] Use str.format instead of string addition --- homeassistant/components/isy994.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d773c492ec4188..7a73e5f6bfd277 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -294,7 +294,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" for domain in SUPPORTED_DOMAINS: try: - folder = programs[KEY_MY_PROGRAMS]['HA.' + domain] + folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] except KeyError: pass else: @@ -314,9 +314,9 @@ def _categorize_weather(hass: HomeAssistant, climate) -> None: """Categorize the ISY994 weather data.""" climate_attrs = dir(climate) weather_nodes = [WeatherNode(getattr(climate, attr), attr, - getattr(climate, attr + '_units')) + 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].append(weather_nodes) From 614b58168f553265ebb740392a1e2afe6d962661 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 20 Dec 2017 16:38:37 -0800 Subject: [PATCH 05/10] Move program structure building and validation to component Removes the need for a bunch of duplicate exception handling in each individual platform --- .../components/binary_sensor/isy994.py | 12 +++------ homeassistant/components/cover/isy994.py | 13 ++-------- homeassistant/components/fan/isy994.py | 13 ++-------- homeassistant/components/isy994.py | 26 +++++++++++++------ homeassistant/components/lock/isy994.py | 13 ++-------- homeassistant/components/switch/isy994.py | 13 ++-------- 6 files changed, 29 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index e1171599da29b4..a5f6cbc1eb1a6d 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -13,7 +13,7 @@ from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - KEY_STATUS, ISYDevice) + 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 @@ -73,14 +73,8 @@ def setup_platform(hass, config: ConfigType, device = ISYBinarySensorDevice(node) devices.append(device) - for program in hass.data[ISY994_PROGRAMS][DOMAIN]: - try: - status = program[KEY_STATUS] - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due to" - "incompatible folder structure.", program.name) - else: - devices.append(ISYBinarySensorProgram(program.name, status)) + for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]: + devices.append(ISYBinarySensorProgram(name, status)) add_devices(devices) diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 4bfd45e691671b..b187b8409c23ca 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -9,7 +9,6 @@ from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - KEY_STATUS, KEY_ACTIONS, ISYDevice) from homeassistant.const import ( STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN) @@ -34,16 +33,8 @@ def setup_platform(hass, config: ConfigType, for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYCoverDevice(node)) - for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): - try: - status = program[KEY_STATUS] - actions = program[KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due to" - "incompatible folder structure.", program.name) - 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) diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index be6e53ba0da560..137bc400d0d95a 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -11,7 +11,6 @@ SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - KEY_STATUS, KEY_ACTIONS, ISYDevice) from homeassistant.helpers.typing import ConfigType @@ -40,16 +39,8 @@ def setup_platform(hass, config: ConfigType, for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYFanDevice(node)) - for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): - try: - status = program[KEY_STATUS] - actions = program[KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due to" - "incompatible folder structure.", program.name) - 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) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 7a73e5f6bfd277..6c7be019fa2423 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -118,6 +118,7 @@ SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover', 'light', 'switch'] +SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch'] # ISY Scenes are more like Swithes than Hass Scenes # (they can turn off, and report their state) @@ -292,7 +293,7 @@ def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" - for domain in SUPPORTED_DOMAINS: + for domain in SUPPORTED_PROGRAM_DOMAINS: try: folder = programs[KEY_MY_PROGRAMS]['HA.{}'.format(domain)] except KeyError: @@ -300,14 +301,23 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: else: for dtype, _, node_id in folder.children: if dtype is KEY_FOLDER: - program = folder[node_id] + entity_folder = folder[node_id] try: - node = program[KEY_STATUS].leaf - assert node.dtype == 'program', 'Not a program' - except (KeyError, AssertionError): - pass - else: - hass.data[ISY994_PROGRAMS][domain].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(hass: HomeAssistant, climate) -> None: diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 6cdc269251e70d..33e2a0bea251d0 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -9,7 +9,6 @@ from homeassistant.components.lock import LockDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - KEY_ACTIONS, KEY_STATUS, ISYDevice) from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN from homeassistant.helpers.typing import ConfigType @@ -30,16 +29,8 @@ def setup_platform(hass, config: ConfigType, for node in hass.data[ISY994_NODES][DOMAIN]: devices.append(ISYLockDevice(node)) - for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): - try: - status = program[KEY_STATUS] - actions = program[KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due to" - "incompatible folder structure.", program.name) - 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) diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 240a994626f6b3..f0fd397710e1b0 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -9,7 +9,6 @@ from homeassistant.components.switch import SwitchDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - KEY_ACTIONS, KEY_STATUS, ISYDevice) from homeassistant.helpers.typing import ConfigType # noqa @@ -25,16 +24,8 @@ def setup_platform(hass, config: ConfigType, if not node.dimmable: devices.append(ISYSwitchDevice(node)) - for program in hass.data[ISY994_PROGRAMS].get(DOMAIN, []): - try: - status = program[KEY_STATUS] - actions = program[KEY_ACTIONS] - assert actions.dtype == 'program', 'Not a program' - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program '%s' failed to load due to " - "incompatible folder structure.", program.name) - 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) From 519aae9894237b741249d327dd04991005b93c55 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 20 Dec 2017 17:08:28 -0800 Subject: [PATCH 06/10] 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. --- homeassistant/components/isy994.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 6c7be019fa2423..d55a53dc875001 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -26,6 +26,7 @@ CONF_IGNORE_STRING = 'ignore_string' CONF_SENSOR_STRING = 'sensor_string' +CONF_ENABLE_CLIMATE = 'enable_climate' CONF_TLS_VER = 'tls' DEFAULT_IGNORE_STRING = '{IGNORE ME}' @@ -45,7 +46,9 @@ 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) @@ -323,11 +326,14 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: def _categorize_weather(hass: HomeAssistant, climate) -> None: """Categorize the ISY994 weather data.""" climate_attrs = dir(climate) - weather_nodes = [WeatherNode(getattr(climate, attr), attr, + _LOGGER.debug(climate) + _LOGGER.debug(climate_attrs) + weather_nodes = [WeatherNode(getattr(climate, attr), + attr.replace('_', ' '), getattr(climate, '{}_units'.format(attr))) for attr in climate_attrs if '{}_units'.format(attr) in climate_attrs] - hass.data[ISY994_WEATHER].append(weather_nodes) + hass.data[ISY994_WEATHER].extend(weather_nodes) def setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -339,7 +345,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[ISY994_NODES][domain] = [] if ISY994_WEATHER not in hass.data: - hass.data[ISY994_WEATHER] = {} + hass.data[ISY994_WEATHER] = [] if ISY994_PROGRAMS not in hass.data: hass.data[ISY994_PROGRAMS] = {} @@ -355,6 +361,7 @@ 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 @@ -376,7 +383,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _categorize_nodes(hass, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass, isy.programs) - if isy.configuration.get('Weather Information'): + if enable_climate and isy.configuration.get('Weather Information'): _categorize_weather(hass, isy.climate) def stop(event: object) -> None: From cc000d5a0187d50d09171ca61fe847a4991604b7 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 20 Dec 2017 17:19:55 -0800 Subject: [PATCH 07/10] Space missing in error message --- homeassistant/components/isy994.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d55a53dc875001..64c54532445f7c 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -314,7 +314,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: else: actions = None except (AttributeError, KeyError, AssertionError): - _LOGGER.warning("Program entity '%s' not loaded due" + _LOGGER.warning("Program entity '%s' not loaded due " "to invalid folder structure.", entity_folder.name) continue From b9f18129802700ac45ee833818a71d006ad2f8ad Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Thu, 21 Dec 2017 13:22:15 -0800 Subject: [PATCH 08/10] Fix string comparison to use `==` --- homeassistant/components/isy994.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 64c54532445f7c..7f178335177f08 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -303,7 +303,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: pass else: for dtype, _, node_id in folder.children: - if dtype is KEY_FOLDER: + if dtype == KEY_FOLDER: entity_folder = folder[node_id] try: status = entity_folder[KEY_STATUS] From 61db17c586aae9f1e4a0f9f9906fc4534bc70e4e Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Sun, 24 Dec 2017 10:14:08 -0800 Subject: [PATCH 09/10] Explicitly check for attributes rather than catch AttributeError Also removes two stray debug lines --- homeassistant/components/isy994.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 7f178335177f08..6e6c9d115d5a7e 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -141,12 +141,12 @@ def _check_for_node_def(hass: HomeAssistant, node, This is only present on the 5.0 ISY firmware, and is the most reliable way to determine a device's type. """ - try: - node_def_id = node.node_def_id - except AttributeError: - # Node doesn't have a node_def + 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 + node_def_id = node.node_def_id + 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']: @@ -164,12 +164,11 @@ def _check_for_insteon_type(hass: HomeAssistant, node, works for Insteon device. "Node Server" (v5+) and Z-Wave and others will not have a type. """ - try: - device_type = node.type - except AttributeError: + 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 @@ -187,12 +186,12 @@ def _check_for_uom_id(hass: HomeAssistant, node, 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. """ - try: - node_uom = set(node.uom) - except AttributeError: + 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) @@ -216,12 +215,12 @@ def _check_for_states_in_uom(hass: HomeAssistant, node, possible "human readable" states. This filter passes if all of the possible states fit inside the given filter. """ - try: - node_uom = set(map(str.lower, node.uom)) - except AttributeError: - # Node doesn't have a uom + 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) @@ -326,8 +325,6 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: def _categorize_weather(hass: HomeAssistant, climate) -> None: """Categorize the ISY994 weather data.""" climate_attrs = dir(climate) - _LOGGER.debug(climate) - _LOGGER.debug(climate_attrs) weather_nodes = [WeatherNode(getattr(climate, attr), attr.replace('_', ' '), getattr(climate, '{}_units'.format(attr))) From 7185a65418755fdd322fb29714cb6956f1ccf090 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 25 Dec 2017 19:43:00 -0800 Subject: [PATCH 10/10] Remove null checks on hass.data, as they are always null at this point --- homeassistant/components/isy994.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 6e6c9d115d5a7e..28cfac391547fe 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -335,20 +335,15 @@ def _categorize_weather(hass: HomeAssistant, climate) -> None: def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ISY 994 platform.""" - if ISY994_NODES not in hass.data: - hass.data[ISY994_NODES] = {} + hass.data[ISY994_NODES] = {} for domain in SUPPORTED_DOMAINS: - if domain not in hass.data[ISY994_NODES]: - hass.data[ISY994_NODES][domain] = [] + hass.data[ISY994_NODES][domain] = [] - if ISY994_WEATHER not in hass.data: - hass.data[ISY994_WEATHER] = [] + hass.data[ISY994_WEATHER] = [] - if ISY994_PROGRAMS not in hass.data: - hass.data[ISY994_PROGRAMS] = {} - for domain in SUPPORTED_DOMAINS: - if domain not in hass.data[ISY994_PROGRAMS]: - hass.data[ISY994_PROGRAMS][domain] = [] + hass.data[ISY994_PROGRAMS] = {} + for domain in SUPPORTED_DOMAINS: + hass.data[ISY994_PROGRAMS][domain] = [] isy_config = config.get(DOMAIN)