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
This commit is contained in:
shbatm 2020-05-04 19:21:40 -05:00 committed by GitHub
parent dd715fcc3a
commit 4be1006b71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 840 additions and 480 deletions

View File

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

View File

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

View File

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

View File

@ -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": "",
"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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"]
}

View File

@ -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": "",
"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)

View File

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