diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 247ea0b231a..14168907224 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -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 diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 4dd1c9be364..b187b8409c2 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -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 diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py index a49952569a8..137bc400d0d 100644 --- a/homeassistant/components/fan/isy994.py +++ b/homeassistant/components/fan/isy994.py @@ -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 diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index af1846c7bf8..28cfac39154 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -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 diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 78b92fbd145..a6191b05c7c 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -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: diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 63272b90b1f..33e2a0bea25 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -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 diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index e961c63a1b5..76f026bba10 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -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: diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 0f1ec62eaee..f0fd397710e 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -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