From 4be1006b71ab3e52d9e0a66b00bdb56083157361 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 4 May 2020 19:21:40 -0500 Subject: [PATCH] ISY994 migration to PyISY v2 (Structure Changes to enable upgrade, Part 1) (#35212) * ISY994 Structure updates in prep for PyISYv2 (Part 1) - Correct node categorization. - Move constants to separate file. - Consolidate Logging to Constants file. - Use Home Assistant Constants where possible. - Use string literals where possible. - Rename "domain" to "platform" in most places. - Add additional device support (NODE_FILTER updates). * Update categorize_programs per review * add @shbatm as codeowner for ISY994 integration --- CODEOWNERS | 2 +- homeassistant/components/isy994/__init__.py | 243 +++------ .../components/isy994/binary_sensor.py | 21 +- homeassistant/components/isy994/const.py | 479 ++++++++++++++++++ homeassistant/components/isy994/cover.py | 29 +- homeassistant/components/isy994/fan.py | 10 +- homeassistant/components/isy994/helpers.py | 249 +++++++++ homeassistant/components/isy994/light.py | 12 +- homeassistant/components/isy994/lock.py | 10 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/sensor.py | 253 +-------- homeassistant/components/isy994/switch.py | 10 +- 12 files changed, 840 insertions(+), 480 deletions(-) create mode 100644 homeassistant/components/isy994/const.py create mode 100644 homeassistant/components/isy994/helpers.py diff --git a/CODEOWNERS b/CODEOWNERS index 7d112e919a70..bf1940059594 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -196,7 +196,7 @@ homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 -homeassistant/components/isy994/* @bdraco +homeassistant/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f0766c4e4f9b..61bdebadd2b8 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,40 +1,47 @@ """Support the ISY-994 controllers.""" from collections import namedtuple -import logging from urllib.parse import urlparse import PyISY from PyISY.Nodes import Group import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - UNIT_PERCENTAGE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, Dict -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "isy994" - -CONF_IGNORE_STRING = "ignore_string" -CONF_SENSOR_STRING = "sensor_string" -CONF_ENABLE_CLIMATE = "enable_climate" -CONF_TLS_VER = "tls" - -DEFAULT_IGNORE_STRING = "{IGNORE ME}" -DEFAULT_SENSOR_STRING = "sensor" - -KEY_ACTIONS = "actions" -KEY_FOLDER = "folder" -KEY_MY_PROGRAMS = "My Programs" -KEY_STATUS = "status" +from .const import ( + _LOGGER, + CONF_ENABLE_CLIMATE, + CONF_IGNORE_STRING, + CONF_SENSOR_STRING, + CONF_TLS_VER, + DEFAULT_IGNORE_STRING, + DEFAULT_SENSOR_STRING, + DOMAIN, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_WEATHER, + ISY_GROUP_PLATFORM, + KEY_ACTIONS, + KEY_FOLDER, + KEY_MY_PROGRAMS, + KEY_STATUS, + NODE_FILTERS, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, +) CONFIG_SCHEMA = vol.Schema( { @@ -57,123 +64,11 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -# Do not use the Home Assistant consts for the states here - we're matching -# exact API responses, not using them for Home Assistant states -NODE_FILTERS = { - "binary_sensor": { - "uom": [], - "states": [], - "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], - "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": ["off", "low", "med", "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", UNIT_PERCENTAGE], - "node_def_id": [ - "DimmerLampSwitch", - "DimmerLampSwitch_ADV", - "DimmerSwitchOnly", - "DimmerSwitchOnly_ADV", - "DimmerLampOnly", - "BallastRelayLampSwitch", - "BallastRelayLampSwitch_ADV", - "RemoteLinc2", - "RemoteLinc2_ADV", - "KeypadDimmer", - "KeypadDimmer_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", - "X10", - "KeypadRelay", - "KeypadRelay_ADV", - ], - "insteon_type": ["2.", "9.10.", "9.11.", "113."], - }, -} - -SUPPORTED_DOMAINS = [ - "binary_sensor", - "sensor", - "lock", - "fan", - "cover", - "light", - "switch", -] -SUPPORTED_PROGRAM_DOMAINS = ["binary_sensor", "lock", "fan", "cover", "switch"] - -# ISY Scenes are more like Switches than Home Assistant 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 _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> bool: - """Check if the node matches the node_def_id for any domains. +def _check_for_node_def(hass: HomeAssistant, node, single_platform: str = None) -> bool: + """Check if the node matches the node_def_id for any platforms. This is only present on the 5.0 ISY firmware, and is the most reliable way to determine a device's type. @@ -184,10 +79,10 @@ def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> 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"]: - hass.data[ISY994_NODES][domain].append(node) + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_def_id in NODE_FILTERS[platform]["node_def_id"]: + hass.data[ISY994_NODES][platform].append(node) return True _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) @@ -195,9 +90,9 @@ def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> def _check_for_insteon_type( - hass: HomeAssistant, node, single_domain: str = None + hass: HomeAssistant, node, single_platform: str = None ) -> bool: - """Check if the node matches the Insteon type for any domains. + """Check if the node matches the Insteon type for any platforms. 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 @@ -208,32 +103,32 @@ def _check_for_insteon_type( return False device_type = node.type - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: if any( [ device_type.startswith(t) - for t in set(NODE_FILTERS[domain]["insteon_type"]) + for t in set(NODE_FILTERS[platform]["insteon_type"]) ] ): # Hacky special-case just for FanLinc, which has a light module # as one of its nodes. Note that this special-case is not necessary # on ISY 5.x firmware as it uses the superior NodeDefs method - if domain == "fan" and int(node.nid[-1]) == 1: - hass.data[ISY994_NODES]["light"].append(node) + if platform == FAN and int(node.nid[-1]) == 1: + hass.data[ISY994_NODES][LIGHT].append(node) return True - hass.data[ISY994_NODES][domain].append(node) + hass.data[ISY994_NODES][platform].append(node) return True return False def _check_for_uom_id( - hass: HomeAssistant, node, single_domain: str = None, uom_list: list = None + hass: HomeAssistant, node, single_platform: str = None, uom_list: list = None ) -> bool: - """Check if a node's uom matches any of the domains uom filter. + """Check if a node's uom matches any of the platforms 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. @@ -246,20 +141,20 @@ def _check_for_uom_id( if uom_list: if node_uom.intersection(uom_list): - hass.data[ISY994_NODES][single_domain].append(node) + hass.data[ISY994_NODES][single_platform].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) + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom.intersection(NODE_FILTERS[platform]["uom"]): + hass.data[ISY994_NODES][platform].append(node) return True return False def _check_for_states_in_uom( - hass: HomeAssistant, node, single_domain: str = None, states_list: list = None + hass: HomeAssistant, node, single_platform: str = None, states_list: list = None ) -> bool: """Check if a list of uoms matches two possible filters. @@ -275,13 +170,13 @@ def _check_for_states_in_uom( if states_list: if node_uom == set(states_list): - hass.data[ISY994_NODES][single_domain].append(node) + hass.data[ISY994_NODES][single_platform].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) + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom == set(NODE_FILTERS[platform]["states"]): + hass.data[ISY994_NODES][platform].append(node) return True return False @@ -289,9 +184,9 @@ def _check_for_states_in_uom( 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"): + if _check_for_node_def(hass, node, single_platform=BINARY_SENSOR): return True - if _check_for_insteon_type(hass, node, single_domain="binary_sensor"): + if _check_for_insteon_type(hass, node, single_platform=BINARY_SENSOR): return True # For the next two checks, we're providing our own set of uoms that @@ -299,11 +194,11 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: # 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"] + hass, node, single_platform=BINARY_SENSOR, uom_list=["2", "78"] ): return True if _check_for_states_in_uom( - hass, node, single_domain="binary_sensor", states_list=["on", "off"] + hass, node, single_platform=BINARY_SENSOR, states_list=["on", "off"] ): return True @@ -313,7 +208,7 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: def _categorize_nodes( hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str ) -> None: - """Sort the nodes to their proper domains.""" + """Sort the nodes to their proper platforms.""" for (path, node) in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: @@ -321,7 +216,7 @@ def _categorize_nodes( continue if isinstance(node, Group): - hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) + hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) continue if sensor_identifier in path or sensor_identifier in node.name: @@ -330,7 +225,7 @@ def _categorize_nodes( if _is_sensor_a_binary_sensor(hass, node): continue - hass.data[ISY994_NODES]["sensor"].append(node) + hass.data[ISY994_NODES][SENSOR].append(node) continue # We have a bunch of different methods for determining the device type, @@ -348,9 +243,9 @@ def _categorize_nodes( def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" - for domain in SUPPORTED_PROGRAM_DOMAINS: + for platform in SUPPORTED_PROGRAM_PLATFORMS: try: - folder = programs[KEY_MY_PROGRAMS][f"HA.{domain}"] + folder = programs[KEY_MY_PROGRAMS][f"HA.{platform}"] except KeyError: pass else: @@ -361,7 +256,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: try: status = entity_folder[KEY_STATUS] assert status.dtype == "program", "Not a program" - if domain != "binary_sensor": + if platform != BINARY_SENSOR: actions = entity_folder[KEY_ACTIONS] assert actions.dtype == "program", "Not a program" else: @@ -375,7 +270,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: continue entity = (entity_folder.name, status, actions) - hass.data[ISY994_PROGRAMS][domain].append(entity) + hass.data[ISY994_PROGRAMS][platform].append(entity) def _categorize_weather(hass: HomeAssistant, climate) -> None: @@ -396,14 +291,14 @@ def _categorize_weather(hass: HomeAssistant, climate) -> None: 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] = [] + for platform in SUPPORTED_PLATFORMS: + hass.data[ISY994_NODES][platform] = [] hass.data[ISY994_WEATHER] = [] hass.data[ISY994_PROGRAMS] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_PROGRAMS][domain] = [] + for platform in SUPPORTED_PROGRAM_PLATFORMS: + hass.data[ISY994_PROGRAMS][platform] = [] isy_config = config.get(DOMAIN) @@ -452,8 +347,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) # Load platforms for the devices in the ISY controller that we support. - for component in SUPPORTED_DOMAINS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in SUPPORTED_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) isy.auto_update = True return True diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index a96f2f44fdbb..093f0f0574e2 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,9 +1,11 @@ """Support for ISY994 binary sensors.""" from datetime import timedelta -import logging from typing import Callable -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR, + BinarySensorEntity, +) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -11,14 +13,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -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"], - "motion": ["16.1", "16.4", "16.5", "16.3"], -} +from .const import _LOGGER, ISY_BIN_SENS_DEVICE_TYPES def setup_platform( @@ -29,7 +24,7 @@ def setup_platform( devices_by_nid = {} child_nodes = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][BINARY_SENSOR]: if node.parent_node is None: device = ISYBinarySensorEntity(node) devices.append(device) @@ -69,7 +64,7 @@ def setup_platform( device = ISYBinarySensorEntity(node) devices.append(device) - for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]: + for name, status, _ in hass.data[ISY994_PROGRAMS][BINARY_SENSOR]: devices.append(ISYBinarySensorProgram(name, status)) add_entities(devices) @@ -83,7 +78,7 @@ def _detect_device_type(node) -> str: return None split_type = device_type.split(".") - for device_class, ids in ISY_DEVICE_TYPES.items(): + for device_class, ids in ISY_BIN_SENS_DEVICE_TYPES.items(): if f"{split_type[0]}.{split_type[1]}" in ids: return device_class diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py new file mode 100644 index 000000000000..ee50a4ef0df0 --- /dev/null +++ b/homeassistant/components/isy994/const.py @@ -0,0 +1,479 @@ +"""Constants for the ISY994 Platform.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_ON, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_BOOST, +) +from homeassistant.components.cover import DOMAIN as COVER +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + MASS_KILOGRAMS, + MASS_POUNDS, + POWER_WATT, + PRESSURE_INHG, + SERVICE_LOCK, + SERVICE_UNLOCK, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + STATE_CLOSED, + STATE_CLOSING, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_OPENING, + STATE_PROBLEM, + STATE_UNKNOWN, + STATE_UNLOCKED, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, + TIME_DAYS, + TIME_HOURS, + TIME_MILLISECONDS, + TIME_MINUTES, + TIME_MONTHS, + TIME_SECONDS, + TIME_YEARS, + UNIT_PERCENTAGE, + UV_INDEX, + VOLT, + VOLUME_GALLONS, + VOLUME_LITERS, +) + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "isy994" + +MANUFACTURER = "Universal Devices, Inc" + +CONF_IGNORE_STRING = "ignore_string" +CONF_SENSOR_STRING = "sensor_string" +CONF_ENABLE_CLIMATE = "enable_climate" +CONF_TLS_VER = "tls" + +DEFAULT_IGNORE_STRING = "{IGNORE ME}" +DEFAULT_SENSOR_STRING = "sensor" +DEFAULT_TLS_VERSION = 1.1 + +KEY_ACTIONS = "actions" +KEY_FOLDER = "folder" +KEY_MY_PROGRAMS = "My Programs" +KEY_STATUS = "status" + +SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH] +SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] + +SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] + +# ISY Scenes are more like Switches than Home Assistant Scenes +# (they can turn off, and report their state) +ISY_GROUP_PLATFORM = SWITCH + +ISY994_ISY = "isy" +ISY994_NODES = "isy994_nodes" +ISY994_WEATHER = "isy994_weather" +ISY994_PROGRAMS = "isy994_programs" + +# Do not use the Home Assistant consts for the states here - we're matching exact API +# responses, not using them for Home Assistant states +NODE_FILTERS = { + BINARY_SENSOR: { + "uom": [], + "states": [], + "node_def_id": [ + "BinaryAlarm", + "BinaryAlarm_ADV", + "BinaryControl", + "BinaryControl_ADV", + "EZIO2x4_Input", + "EZRAIN_Input", + "OnOffControl", + "OnOffControl_ADV", + ], + "insteon_type": [ + "7.0.", + "7.13.", + "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", "EZIO2x4_Input_ADV"], + "insteon_type": ["9.0.", "9.7."], + }, + LOCK: { + "uom": ["11"], + "states": ["locked", "unlocked"], + "node_def_id": ["DoorLock"], + "insteon_type": ["15.", "4.64."], + }, + FAN: { + "uom": [], + "states": ["off", "low", "med", "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": [ + "BallastRelayLampSwitch", + "BallastRelayLampSwitch_ADV", + "DimmerLampOnly", + "DimmerLampSwitch", + "DimmerLampSwitch_ADV", + "DimmerSwitchOnly", + "DimmerSwitchOnly_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", + ], + "insteon_type": ["1."], + }, + SWITCH: { + "uom": ["2", "78"], + "states": ["on", "off"], + "node_def_id": [ + "AlertModuleArmed", + "AlertModuleSiren", + "AlertModuleSiren_ADV", + "EZIO2x4_Output", + "EZRAIN_Output", + "KeypadButton", + "KeypadButton_ADV", + "KeypadRelay", + "KeypadRelay_ADV", + "RelayLampOnly", + "RelayLampOnly_ADV", + "RelayLampSwitch", + "RelayLampSwitch_ADV", + "RelaySwitchOnlyPlusQuery", + "RelaySwitchOnlyPlusQuery_ADV", + "RemoteLinc2", + "RemoteLinc2_ADV", + "Siren", + "Siren_ADV", + "X10", + ], + "insteon_type": ["0.16.", "2.", "7.3.255.", "9.10.", "9.11.", "113."], + }, +} + +UOM_FRIENDLY_NAME = { + "1": "A", + "3": f"btu/{TIME_HOURS}", + "4": TEMP_CELSIUS, + "5": LENGTH_CENTIMETERS, + "6": "ft³", + "7": f"ft³/{TIME_MINUTES}", + "8": "m³", + "9": TIME_DAYS, + "10": TIME_DAYS, + "12": "dB", + "13": "dB A", + "14": DEGREE, + "16": "macroseismic", + "17": TEMP_FAHRENHEIT, + "18": LENGTH_FEET, + "19": TIME_HOURS, + "20": TIME_HOURS, + "21": "%AH", + "22": "%RH", + "23": PRESSURE_INHG, + "24": f"{LENGTH_INCHES}/{TIME_HOURS}", + "25": "index", + "26": TEMP_KELVIN, + "27": "keyword", + "28": MASS_KILOGRAMS, + "29": "kV", + "30": "kW", + "31": "kPa", + "32": SPEED_KILOMETERS_PER_HOUR, + "33": ENERGY_KILO_WATT_HOUR, + "34": "liedu", + "35": VOLUME_LITERS, + "36": "lx", + "37": "mercalli", + "38": LENGTH_METERS, + "39": f"{LENGTH_METERS}³/{TIME_HOURS}", + "40": SPEED_METERS_PER_SECOND, + "41": "mA", + "42": TIME_MILLISECONDS, + "43": "mV", + "44": TIME_MINUTES, + "45": TIME_MINUTES, + "46": f"mm/{TIME_HOURS}", + "47": TIME_MONTHS, + "48": SPEED_MILES_PER_HOUR, + "49": SPEED_METERS_PER_SECOND, + "50": "Ω", + "51": UNIT_PERCENTAGE, + "52": MASS_POUNDS, + "53": "pf", + "54": CONCENTRATION_PARTS_PER_MILLION, + "55": "pulse count", + "57": TIME_SECONDS, + "58": TIME_SECONDS, + "59": "S/m", + "60": "m_b", + "61": "M_L", + "62": "M_w", + "63": "M_S", + "64": "shindo", + "65": "SML", + "69": VOLUME_GALLONS, + "71": UV_INDEX, + "72": VOLT, + "73": POWER_WATT, + "74": f"{POWER_WATT}/{LENGTH_METERS}²", + "75": "weekday", + "76": DEGREE, + "77": TIME_YEARS, + "82": "mm", + "83": LENGTH_KILOMETERS, + "85": "Ω", + "86": "kΩ", + "87": f"{LENGTH_METERS}³/{LENGTH_METERS}³", + "88": "Water activity", + "89": "RPM", + "90": FREQUENCY_HERTZ, + "91": DEGREE, + "92": f"{DEGREE} South", + "101": f"{DEGREE} (x2)", + "102": "kWs", + "103": "$", + "104": "¢", + "105": LENGTH_INCHES, + "106": "mm/day", +} + +UOM_TO_STATES = { + "11": { # Deadbolt Status + 0: STATE_UNLOCKED, + 100: STATE_LOCKED, + 101: STATE_UNKNOWN, + 102: STATE_PROBLEM, + }, + "15": { # Door Lock Alarm + 1: "master code changed", + 2: "tamper code entry limit", + 3: "escutcheon removed", + 4: "key/manually locked", + 5: "locked by touch", + 6: "key/manually unlocked", + 7: "remote locking jammed bolt", + 8: "remotely locked", + 9: "remotely unlocked", + 10: "deadbolt jammed", + 11: "battery too low to operate", + 12: "critical low battery", + 13: "low battery", + 14: "automatically locked", + 15: "automatic locking jammed bolt", + 16: "remotely power cycled", + 17: "lock handling complete", + 19: "user deleted", + 20: "user added", + 21: "duplicate pin", + 22: "jammed bolt by locking with keypad", + 23: "locked by keypad", + 24: "unlocked by keypad", + 25: "keypad attempt outside schedule", + 26: "hardware failure", + 27: "factory reset", + }, + "66": { # Thermostat Heat/Cool State + 0: CURRENT_HVAC_IDLE, + 1: CURRENT_HVAC_HEAT, + 2: CURRENT_HVAC_COOL, + 3: CURRENT_HVAC_FAN, + 4: CURRENT_HVAC_HEAT, # Pending Heat + 5: CURRENT_HVAC_COOL, # Pending Cool + # >6 defined in ISY but not implemented, leaving for future expanision. + 6: CURRENT_HVAC_IDLE, + 7: CURRENT_HVAC_HEAT, + 8: CURRENT_HVAC_HEAT, + 9: CURRENT_HVAC_COOL, + 10: CURRENT_HVAC_HEAT, + 11: CURRENT_HVAC_HEAT, + }, + "67": { # Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_AUTO, + 4: PRESET_BOOST, + 5: "resume", + 6: HVAC_MODE_FAN_ONLY, + 7: "furnace", + 8: HVAC_MODE_DRY, + 9: "moist air", + 10: "auto changeover", + 11: "energy save heat", + 12: "energy save cool", + 13: PRESET_AWAY, + 14: HVAC_MODE_AUTO, + 15: HVAC_MODE_AUTO, + 16: HVAC_MODE_AUTO, + }, + "68": { # Thermostat Fan Mode + 0: FAN_AUTO, + 1: FAN_ON, + 2: FAN_HIGH, # Auto High + 3: FAN_HIGH, + 4: FAN_MEDIUM, # Auto Medium + 5: FAN_MEDIUM, + 6: "circulation", + 7: "humidity circulation", + }, + "78": {0: STATE_OFF, 100: STATE_ON}, # 0-Off 100-On + "79": {0: STATE_OPEN, 100: STATE_CLOSED}, # 0-Open 100-Close + "80": { # Thermostat Fan Run State + 0: STATE_OFF, + 1: STATE_ON, + 2: "on high", + 3: "on medium", + 4: "circulation", + 5: "humidity circulation", + 6: "right/left circulation", + 7: "up/down circulation", + 8: "quiet circulation", + }, + "84": {0: SERVICE_LOCK, 1: SERVICE_UNLOCK}, # Secure Mode + "93": { # Power Management Alarm + 1: "power applied", + 2: "ac mains disconnected", + 3: "ac mains reconnected", + 4: "surge detection", + 5: "volt drop or drift", + 6: "over current detected", + 7: "over voltage detected", + 8: "over load detected", + 9: "load error", + 10: "replace battery soon", + 11: "replace battery now", + 12: "battery is charging", + 13: "battery is fully charged", + 14: "charge battery soon", + 15: "charge battery now", + }, + "94": { # Appliance Alarm + 1: "program started", + 2: "program in progress", + 3: "program completed", + 4: "replace main filter", + 5: "failure to set target temperature", + 6: "supplying water", + 7: "water supply failure", + 8: "boiling", + 9: "boiling failure", + 10: "washing", + 11: "washing failure", + 12: "rinsing", + 13: "rinsing failure", + 14: "draining", + 15: "draining failure", + 16: "spinning", + 17: "spinning failure", + 18: "drying", + 19: "drying failure", + 20: "fan failure", + 21: "compressor failure", + }, + "95": { # Home Health Alarm + 1: "leaving bed", + 2: "sitting on bed", + 3: "lying on bed", + 4: "posture changed", + 5: "sitting on edge of bed", + }, + "96": { # VOC Level + 1: "clean", + 2: "slightly polluted", + 3: "moderately polluted", + 4: "highly polluted", + }, + "97": { # Barrier Status + **{ + 0: STATE_CLOSED, + 100: STATE_OPEN, + 101: STATE_UNKNOWN, + 102: "stopped", + 103: STATE_CLOSING, + 104: STATE_OPENING, + }, + **{ + b: f"{b} %" for a, b in enumerate(list(range(1, 100))) + }, # 1-99 are percentage open + }, + "98": { # Insteon Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_HEAT_COOL, + 4: HVAC_MODE_FAN_ONLY, + 5: HVAC_MODE_AUTO, # Program Auto + 6: HVAC_MODE_AUTO, # Program Heat-Set @ Local Device Only + 7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only + }, + "99": {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode +} + +ISY_BIN_SENS_DEVICE_TYPES = { + "moisture": ["16.8.", "16.13.", "16.14."], + "opening": ["16.9.", "16.6.", "16.7.", "16.2.", "16.17.", "16.20.", "16.21."], + "motion": ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], + "climate": ["5.11.", "5.10."], +} + +# TEMPORARY CONSTANTS -- REMOVE AFTER PyISYv2 IS AVAILABLE +ISY_VALUE_UNKNOWN = -1 * float("inf") diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 38688db21d27..d5246d1a1341 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,28 +1,12 @@ """Support for ISY994 covers.""" -import logging from typing import Callable -from homeassistant.components.cover import DOMAIN, CoverEntity -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - STATE_UNKNOWN, -) +from homeassistant.components.cover import DOMAIN as COVER, CoverEntity +from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -VALUE_TO_STATE = { - 0: STATE_CLOSED, - 101: STATE_UNKNOWN, - 102: "stopped", - 103: STATE_CLOSING, - 104: STATE_OPENING, -} +from .const import _LOGGER, UOM_TO_STATES def setup_platform( @@ -30,10 +14,10 @@ def setup_platform( ): """Set up the ISY994 cover platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][COVER]: devices.append(ISYCoverEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + for name, status, actions in hass.data[ISY994_PROGRAMS][COVER]: devices.append(ISYCoverProgram(name, status, actions)) add_entities(devices) @@ -59,7 +43,8 @@ class ISYCoverEntity(ISYDevice, CoverEntity): """Get the state of the ISY994 cover device.""" if self.is_unknown(): return None - return VALUE_TO_STATE.get(self.value, STATE_OPEN) + # TEMPORARY: Cast value to int until PyISYv2. + return UOM_TO_STATES["97"].get(int(self.value), STATE_OPEN) def open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover device.""" diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index b42e8cda755f..cf8bd2cb3088 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,9 +1,8 @@ """Support for ISY994 fans.""" -import logging from typing import Callable from homeassistant.components.fan import ( - DOMAIN, + DOMAIN as FAN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -14,8 +13,7 @@ from homeassistant.components.fan import ( from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER VALUE_TO_STATE = { 0: SPEED_OFF, @@ -37,10 +35,10 @@ def setup_platform( """Set up the ISY994 fan platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][FAN]: devices.append(ISYFanDevice(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + for name, status, actions in hass.data[ISY994_PROGRAMS][FAN]: devices.append(ISYFanProgram(name, status, actions)) add_entities(devices) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py new file mode 100644 index 000000000000..4f6bba6b6592 --- /dev/null +++ b/homeassistant/components/isy994/helpers.py @@ -0,0 +1,249 @@ +"""Sorting helpers for ISY994 device classifications.""" +from collections import namedtuple + +from PyISY.Nodes import Group + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_WEATHER, + ISY_GROUP_PLATFORM, + KEY_ACTIONS, + KEY_FOLDER, + KEY_MY_PROGRAMS, + KEY_STATUS, + NODE_FILTERS, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, +) + +WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom")) + + +def _check_for_node_def( + hass: HomeAssistantType, node, single_platform: str = None +) -> bool: + """Check if the node matches the node_def_id for any platforms. + + 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 + + node_def_id = node.node_def_id + + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_def_id in NODE_FILTERS[platform]["node_def_id"]: + hass.data[ISY994_NODES][platform].append(node) + return True + + _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) + return False + + +def _check_for_insteon_type( + hass: HomeAssistantType, node, single_platform: str = None +) -> bool: + """Check if the node matches the Insteon type for any platforms. + + 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 + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if any( + [ + device_type.startswith(t) + for t in set(NODE_FILTERS[platform]["insteon_type"]) + ] + ): + + # Hacky special-case just for FanLinc, which has a light module + # as one of its nodes. Note that this special-case is not necessary + # on ISY 5.x firmware as it uses the superior NodeDefs method + if platform == FAN and int(node.nid[-1]) == 1: + hass.data[ISY994_NODES][LIGHT].append(node) + return True + + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_uom_id( + hass: HomeAssistantType, node, single_platform: str = None, uom_list: list = None +) -> bool: + """Check if a node's uom matches any of the platforms 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(uom_list): + hass.data[ISY994_NODES][single_platform].append(node) + return True + else: + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom.intersection(NODE_FILTERS[platform]["uom"]): + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_states_in_uom( + hass: HomeAssistantType, node, single_platform: 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_platform].append(node) + return True + else: + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom == set(NODE_FILTERS[platform]["states"]): + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _is_sensor_a_binary_sensor(hass: HomeAssistantType, node) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(hass, node, single_platform=BINARY_SENSOR): + return True + if _check_for_insteon_type(hass, node, single_platform=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_platform=BINARY_SENSOR, uom_list=["2", "78"] + ): + return True + if _check_for_states_in_uom( + hass, node, single_platform=BINARY_SENSOR, states_list=["on", "off"] + ): + return True + + return False + + +def _categorize_nodes( + hass: HomeAssistantType, nodes, ignore_identifier: str, sensor_identifier: str +) -> None: + """Sort the nodes to their proper platforms.""" + 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 + + if isinstance(node, Group): + hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].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 + + 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(hass: HomeAssistantType, programs: dict) -> None: + """Categorize the ISY994 programs.""" + for platform in SUPPORTED_PROGRAM_PLATFORMS: + try: + folder = programs[KEY_MY_PROGRAMS][f"HA.{platform}"] + except KeyError: + continue + for dtype, _, node_id in folder.children: + if dtype != KEY_FOLDER: + continue + entity_folder = folder[node_id] + try: + status = entity_folder[KEY_STATUS] + assert status.dtype == "program", "Not a program" + if platform != BINARY_SENSOR: + actions = entity_folder[KEY_ACTIONS] + assert actions.dtype == "program", "Not a program" + else: + actions = None + except (AttributeError, KeyError, AssertionError): + _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][platform].append(entity) + + +def _categorize_weather(hass: HomeAssistantType, climate) -> None: + """Categorize the ISY994 weather data.""" + climate_attrs = dir(climate) + weather_nodes = [ + WeatherNode( + getattr(climate, attr), + attr.replace("_", " "), + getattr(climate, f"{attr}_units"), + ) + for attr in climate_attrs + if f"{attr}_units" in climate_attrs + ] + hass.data[ISY994_WEATHER].extend(weather_nodes) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7ae8d1c76f81..4a6c9ad86126 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,14 +1,16 @@ """Support for ISY994 lights.""" -import logging from typing import Callable -from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.components.light import ( + DOMAIN as LIGHT, + SUPPORT_BRIGHTNESS, + LightEntity, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -18,7 +20,7 @@ def setup_platform( ): """Set up the ISY994 light platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][LIGHT]: devices.append(ISYLightDevice(node)) add_entities(devices) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 807027d46105..122123be71f8 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,14 +1,12 @@ """Support for ISY994 locks.""" -import logging from typing import Callable -from homeassistant.components.lock import DOMAIN, LockEntity +from homeassistant.components.lock import DOMAIN as LOCK, LockEntity from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER VALUE_TO_STATE = {0: STATE_UNLOCKED, 100: STATE_LOCKED} @@ -18,10 +16,10 @@ def setup_platform( ): """Set up the ISY994 lock platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][LOCK]: devices.append(ISYLockDevice(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + for name, status, actions in hass.data[ISY994_PROGRAMS][LOCK]: devices.append(ISYLockProgram(name, status, actions)) add_entities(devices) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 083f25808fbc..88de7db824cd 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,5 +3,5 @@ "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": ["PyISY==1.1.2"], - "codeowners": ["@bdraco"] + "codeowners": ["@bdraco", "@shbatm"] } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 1252d0ef53b4..d98020c6db86 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,252 +1,12 @@ """Support for ISY994 sensors.""" -import logging from typing import Callable -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - DEGREE, - FREQUENCY_HERTZ, - LENGTH_CENTIMETERS, - LENGTH_KILOMETERS, - LENGTH_METERS, - MASS_KILOGRAMS, - POWER_WATT, - SPEED_KILOMETERS_PER_HOUR, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_DAYS, - TIME_HOURS, - TIME_MILLISECONDS, - TIME_MINUTES, - TIME_MONTHS, - TIME_SECONDS, - TIME_YEARS, - UNIT_PERCENTAGE, - UV_INDEX, - VOLT, -) +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISY994_WEATHER, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -UOM_FRIENDLY_NAME = { - "1": "amp", - "3": f"btu/{TIME_HOURS}", - "4": TEMP_CELSIUS, - "5": LENGTH_CENTIMETERS, - "6": "ft³", - "7": f"ft³/{TIME_MINUTES}", - "8": "m³", - "9": TIME_DAYS, - "10": TIME_DAYS, - "12": "dB", - "13": "dB A", - "14": DEGREE, - "16": "macroseismic", - "17": TEMP_FAHRENHEIT, - "18": "ft", - "19": TIME_HOURS, - "20": TIME_HOURS, - "21": "abs. humidity (%)", - "22": "rel. humidity (%)", - "23": "inHg", - "24": "in/hr", - "25": "index", - "26": "K", - "27": "keyword", - "28": MASS_KILOGRAMS, - "29": "kV", - "30": "kW", - "31": "kPa", - "32": SPEED_KILOMETERS_PER_HOUR, - "33": "kWH", - "34": "liedu", - "35": "l", - "36": "lx", - "37": "mercalli", - "38": LENGTH_METERS, - "39": "m³/hr", - "40": SPEED_METERS_PER_SECOND, - "41": "mA", - "42": TIME_MILLISECONDS, - "43": "mV", - "44": TIME_MINUTES, - "45": TIME_MINUTES, - "46": "mm/hr", - "47": TIME_MONTHS, - "48": SPEED_MILES_PER_HOUR, - "49": SPEED_METERS_PER_SECOND, - "50": "ohm", - "51": UNIT_PERCENTAGE, - "52": "lb", - "53": "power factor", - "54": CONCENTRATION_PARTS_PER_MILLION, - "55": "pulse count", - "57": TIME_SECONDS, - "58": TIME_SECONDS, - "59": "seimens/m", - "60": "body wave magnitude scale", - "61": "Ricter scale", - "62": "moment magnitude scale", - "63": "surface wave magnitude scale", - "64": "shindo", - "65": "SML", - "69": "gal", - "71": UV_INDEX, - "72": VOLT, - "73": POWER_WATT, - "74": f"{POWER_WATT}/m²", - "75": "weekday", - "76": f"Wind Direction ({DEGREE})", - "77": TIME_YEARS, - "82": "mm", - "83": LENGTH_KILOMETERS, - "85": "ohm", - "86": "kOhm", - "87": "m³/m³", - "88": "Water activity", - "89": "RPM", - "90": FREQUENCY_HERTZ, - "91": f"{DEGREE} (Relative to North)", - "92": f"{DEGREE} (Relative to South)", -} - -UOM_TO_STATES = { - "11": {"0": "unlocked", "100": "locked", "102": "jammed"}, - "15": { - "1": "master code changed", - "2": "tamper code entry limit", - "3": "escutcheon removed", - "4": "key/manually locked", - "5": "locked by touch", - "6": "key/manually unlocked", - "7": "remote locking jammed bolt", - "8": "remotely locked", - "9": "remotely unlocked", - "10": "deadbolt jammed", - "11": "battery too low to operate", - "12": "critical low battery", - "13": "low battery", - "14": "automatically locked", - "15": "automatic locking jammed bolt", - "16": "remotely power cycled", - "17": "lock handling complete", - "19": "user deleted", - "20": "user added", - "21": "duplicate pin", - "22": "jammed bolt by locking with keypad", - "23": "locked by keypad", - "24": "unlocked by keypad", - "25": "keypad attempt outside schedule", - "26": "hardware failure", - "27": "factory reset", - }, - "66": { - "0": "idle", - "1": "heating", - "2": "cooling", - "3": "fan only", - "4": "pending heat", - "5": "pending cool", - "6": "vent", - "7": "aux heat", - "8": "2nd stage heating", - "9": "2nd stage cooling", - "10": "2nd stage aux heat", - "11": "3rd stage aux heat", - }, - "67": { - "0": "off", - "1": "heat", - "2": "cool", - "3": "auto", - "4": "aux/emergency heat", - "5": "resume", - "6": "fan only", - "7": "furnace", - "8": "dry air", - "9": "moist air", - "10": "auto changeover", - "11": "energy save heat", - "12": "energy save cool", - "13": "away", - }, - "68": { - "0": "auto", - "1": "on", - "2": "auto high", - "3": "high", - "4": "auto medium", - "5": "medium", - "6": "circulation", - "7": "humidity circulation", - }, - "93": { - "1": "power applied", - "2": "ac mains disconnected", - "3": "ac mains reconnected", - "4": "surge detection", - "5": "volt drop or drift", - "6": "over current detected", - "7": "over voltage detected", - "8": "over load detected", - "9": "load error", - "10": "replace battery soon", - "11": "replace battery now", - "12": "battery is charging", - "13": "battery is fully charged", - "14": "charge battery soon", - "15": "charge battery now", - }, - "94": { - "1": "program started", - "2": "program in progress", - "3": "program completed", - "4": "replace main filter", - "5": "failure to set target temperature", - "6": "supplying water", - "7": "water supply failure", - "8": "boiling", - "9": "boiling failure", - "10": "washing", - "11": "washing failure", - "12": "rinsing", - "13": "rinsing failure", - "14": "draining", - "15": "draining failure", - "16": "spinning", - "17": "spinning failure", - "18": "drying", - "19": "drying failure", - "20": "fan failure", - "21": "compressor failure", - }, - "95": { - "1": "leaving bed", - "2": "sitting on bed", - "3": "lying on bed", - "4": "posture changed", - "5": "sitting on edge of bed", - }, - "96": { - "1": "clean", - "2": "slightly polluted", - "3": "moderately polluted", - "4": "highly polluted", - }, - "97": { - "0": "closed", - "100": "open", - "102": "stopped", - "103": "closing", - "104": "opening", - }, -} +from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES def setup_platform( @@ -255,7 +15,7 @@ def setup_platform( """Set up the ISY994 sensor platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][SENSOR]: _LOGGER.debug("Loading %s", node.name) devices.append(ISYSensorDevice(node)) @@ -289,8 +49,9 @@ class ISYSensorDevice(ISYDevice): if len(self._node.uom) == 1: if self._node.uom[0] in UOM_TO_STATES: states = UOM_TO_STATES.get(self._node.uom[0]) - if self.value in states: - return states.get(self.value) + # TEMPORARY: Cast value to int until PyISYv2. + if int(self.value) in states: + return states.get(int(self.value)) elif self._node.prec and self._node.prec != [0]: str_val = str(self.value) int_prec = int(self._node.prec) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index d5339960282b..564d7cbaeac3 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,13 +1,11 @@ """Support for ISY994 switches.""" -import logging from typing import Callable -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from .const import _LOGGER def setup_platform( @@ -15,11 +13,11 @@ def setup_platform( ): """Set up the ISY994 switch platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][SWITCH]: if not node.dimmable: devices.append(ISYSwitchDevice(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: + for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]: devices.append(ISYSwitchProgram(name, status, actions)) add_entities(devices)