Add ISY programs and support for all device types (#3082)

*  ISY Lock, Binary Sensor, Cover devices, Sensors and Fan support
* Support for ISY Programs
This commit is contained in:
Teagan Glenn 2016-09-11 12:18:53 -06:00 committed by Johann Kellerman
parent 8c7a1b4b05
commit 05a3b610ff
9 changed files with 1057 additions and 321 deletions

View File

@ -0,0 +1,76 @@
"""
Support for ISY994 binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.isy994/
"""
import logging
from typing import Callable # noqa
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Setup the ISY994 binary sensor platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
devices = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
states=STATES):
devices.append(ISYBinarySensorDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
except (KeyError, AssertionError):
pass
else:
devices.append(ISYBinarySensorProgram(program.name, status))
add_devices(devices)
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
"""Representation of an ISY994 binary sensor device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 binary sensor device."""
isy.ISYDevice.__init__(self, node)
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on."""
return bool(self.state)
class ISYBinarySensorProgram(ISYBinarySensorDevice):
"""Representation of an ISY994 binary sensor program."""
def __init__(self, name, node) -> None:
"""Initialize the ISY994 binary sensor program."""
ISYBinarySensorDevice.__init__(self, node)
self._name = name
@property
def is_on(self):
"""Get whether the ISY994 binary sensor program is on."""
return bool(self.value)

View File

@ -0,0 +1,109 @@
"""
Support for ISY994 covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.isy994/
"""
import logging
from typing import Callable # noqa
from homeassistant.components.cover import CoverDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
0: STATE_CLOSED,
101: STATE_UNKNOWN,
}
UOM = ['97']
STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening']
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Setup the ISY994 cover platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
devices = []
for node in isy.filter_nodes(isy.NODES, units=UOM,
states=STATES):
devices.append(ISYCoverDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
devices.append(ISYCoverProgram(program.name, status, actions))
add_devices(devices)
class ISYCoverDevice(isy.ISYDevice, CoverDevice):
"""Representation of an ISY994 cover device."""
def __init__(self, node: object):
"""Initialize the ISY994 cover device."""
isy.ISYDevice.__init__(self, node)
@property
def current_cover_position(self) -> int:
"""Get the current cover position."""
return sorted((0, self.value, 100))[1]
@property
def is_closed(self) -> bool:
"""Get whether the ISY994 cover device is closed."""
return self.state == STATE_CLOSED
@property
def state(self) -> str:
"""Get the state of the ISY994 cover device."""
return VALUE_TO_STATE.get(self.value, STATE_OPEN)
def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device."""
if not self._node.on(val=100):
_LOGGER.error('Unable to open the cover')
def close_cover(self, **kwargs) -> None:
"""Send the close cover command to the ISY994 cover device."""
if not self._node.off():
_LOGGER.error('Unable to close the cover')
class ISYCoverProgram(ISYCoverDevice):
"""Representation of an ISY994 cover program."""
def __init__(self, name: str, node: object, actions: object) -> None:
"""Initialize the ISY994 cover program."""
ISYCoverDevice.__init__(self, node)
self._name = name
self._actions = actions
@property
def state(self) -> str:
"""Get the state of the ISY994 cover program."""
return STATE_CLOSED if bool(self.value) else STATE_OPEN
def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover program."""
if not self._actions.runThen():
_LOGGER.error('Unable to open the cover')
def close_cover(self, **kwargs) -> None:
"""Send the close cover command to the ISY994 cover program."""
if not self._actions.runElse():
_LOGGER.error('Unable to close the cover')

View File

@ -0,0 +1,120 @@
"""
Support for ISY994 fans.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/fan.isy994/
"""
import logging
from typing import Callable
from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
SPEED_LOW, SPEED_MED,
SPEED_HIGH)
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
0: SPEED_OFF,
63: SPEED_LOW,
64: SPEED_LOW,
190: SPEED_MED,
191: SPEED_MED,
255: SPEED_HIGH,
}
STATE_TO_VALUE = {}
for key in VALUE_TO_STATE:
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
STATES = [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH]
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Setup the ISY994 fan platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
devices = []
for node in isy.filter_nodes(isy.NODES, states=STATES):
devices.append(ISYFanDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
devices.append(ISYFanProgram(program.name, status, actions))
add_devices(devices)
class ISYFanDevice(isy.ISYDevice, FanEntity):
"""Representation of an ISY994 fan device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 fan device."""
isy.ISYDevice.__init__(self, node)
self.speed = self.state
@property
def state(self) -> str:
"""Get the state of the ISY994 fan device."""
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def set_speed(self, speed: str) -> None:
"""Send the set speed command to the ISY994 fan device."""
if not self._node.on(val=STATE_TO_VALUE.get(speed, 0)):
_LOGGER.debug('Unable to set fan speed')
else:
self.speed = self.state
def turn_on(self, speed: str=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 fan device."""
self.set_speed(speed)
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 fan device."""
if not self._node.off():
_LOGGER.debug('Unable to set fan speed')
else:
self.speed = self.state
class ISYFanProgram(ISYFanDevice):
"""Representation of an ISY994 fan program."""
def __init__(self, name: str, node, actions) -> None:
"""Initialize the ISY994 fan program."""
ISYFanDevice.__init__(self, node)
self._name = name
self._actions = actions
self.speed = STATE_ON if self.is_on else STATE_OFF
@property
def state(self) -> str:
"""Get the state of the ISY994 fan program."""
return STATE_ON if bool(self.value) else STATE_OFF
def turn_off(self, **kwargs) -> None:
"""Send the turn on command to ISY994 fan program."""
if not self._actions.runThen():
_LOGGER.error('Unable to open the cover')
else:
self.speed = STATE_ON if self.is_on else STATE_OFF
def turn_on(self, **kwargs) -> None:
"""Send the turn off command to ISY994 fan program."""
if not self._actions.runElse():
_LOGGER.error('Unable to close the cover')
else:
self.speed = STATE_ON if self.is_on else STATE_OFF

View File

@ -6,43 +6,150 @@ https://home-assistant.io/components/isy994/
""" """
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
import voluptuous as vol
from homeassistant.core import HomeAssistant # noqa
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config, discovery from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
DOMAIN = "isy994" DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.6'] REQUIREMENTS = ['PyISY==1.0.7']
ISY = None ISY = None
SENSOR_STRING = 'Sensor' DEFAULT_SENSOR_STRING = 'sensor'
HIDDEN_STRING = '{HIDE ME}' DEFAULT_HIDDEN_STRING = '{HIDE ME}'
CONF_TLS_VER = 'tls' CONF_TLS_VER = 'tls'
CONF_HIDDEN_STRING = 'hidden_string'
CONF_SENSOR_STRING = 'sensor_string'
KEY_MY_PROGRAMS = 'My Programs'
KEY_FOLDER = 'folder'
KEY_ACTIONS = 'actions'
KEY_STATUS = 'status'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.url,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TLS_VER): vol.Coerce(float),
vol.Optional(CONF_HIDDEN_STRING,
default=DEFAULT_HIDDEN_STRING): cv.string,
vol.Optional(CONF_SENSOR_STRING,
default=DEFAULT_SENSOR_STRING): cv.string
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config): SENSOR_NODES = []
"""Setup ISY994 component. NODES = []
GROUPS = []
PROGRAMS = {}
This will automatically import associated lights, switches, and sensors. PYISY = None
"""
import PyISY
# pylint: disable=global-statement HIDDEN_STRING = DEFAULT_HIDDEN_STRING
# check for required values in configuration file
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return False
# Pull and parse standard configuration. SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock',
user = config[DOMAIN][CONF_USERNAME] 'sensor', 'switch']
password = config[DOMAIN][CONF_PASSWORD]
host = urlparse(config[DOMAIN][CONF_HOST])
def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
"""Filter a list of ISY nodes based on the units and states provided."""
filtered_nodes = []
units = units if units else []
states = states if states else []
for node in nodes:
match_unit = False
match_state = True
for uom in node.uom:
if uom in units:
match_unit = True
continue
elif uom not in states:
match_state = False
if match_unit:
continue
if match_unit or match_state:
filtered_nodes.append(node)
return filtered_nodes
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
"""Categorize the ISY994 nodes."""
global SENSOR_NODES
global NODES
global GROUPS
SENSOR_NODES = []
NODES = []
GROUPS = []
for (path, node) in ISY.nodes:
hidden = hidden_identifier in path or hidden_identifier in node.name
if hidden:
node.name += hidden_identifier
if sensor_identifier in path or sensor_identifier in node.name:
SENSOR_NODES.append(node)
elif isinstance(node, PYISY.Nodes.Node): # pylint: disable=no-member
NODES.append(node)
elif isinstance(node, PYISY.Nodes.Group): # pylint: disable=no-member
GROUPS.append(node)
def _categorize_programs() -> None:
"""Categorize the ISY994 programs."""
global PROGRAMS
PROGRAMS = {}
for component in SUPPORTED_DOMAINS:
try:
folder = ISY.programs[KEY_MY_PROGRAMS]['HA.' + component]
except KeyError:
pass
else:
for dtype, _, node_id in folder.children:
if dtype is KEY_FOLDER:
program = folder[node_id]
try:
node = program[KEY_STATUS].leaf
assert node.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
if component not in PROGRAMS:
PROGRAMS[component] = []
PROGRAMS[component].append(program)
# pylint: disable=too-many-locals
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the ISY 994 platform."""
isy_config = config.get(DOMAIN)
user = isy_config.get(CONF_USERNAME)
password = isy_config.get(CONF_PASSWORD)
tls_version = isy_config.get(CONF_TLS_VER)
host = urlparse(isy_config.get(CONF_HOST))
port = host.port
addr = host.geturl() addr = host.geturl()
hidden_identifier = isy_config.get(CONF_HIDDEN_STRING,
DEFAULT_HIDDEN_STRING)
sensor_identifier = isy_config.get(CONF_SENSOR_STRING,
DEFAULT_SENSOR_STRING)
global HIDDEN_STRING
HIDDEN_STRING = hidden_identifier
if host.scheme == 'http': if host.scheme == 'http':
addr = addr.replace('http://', '') addr = addr.replace('http://', '')
https = False https = False
@ -50,169 +157,125 @@ def setup(hass, config):
addr = addr.replace('https://', '') addr = addr.replace('https://', '')
https = True https = True
else: else:
_LOGGER.error('isy994 host value in configuration file is invalid.') _LOGGER.error('isy994 host value in configuration is invalid.')
return False return False
port = host.port
addr = addr.replace(':{}'.format(port), '') addr = addr.replace(':{}'.format(port), '')
# Pull and parse optional configuration. import PyISY
global SENSOR_STRING
global HIDDEN_STRING global PYISY
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING)) PYISY = PyISY
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
# Connect to ISY controller. # Connect to ISY controller.
global ISY global ISY
ISY = PyISY.ISY(addr, port, user, password, use_https=https, ISY = PyISY.ISY(addr, port, username=user, password=password,
tls_ver=tls_version, log=_LOGGER) use_https=https, tls_ver=tls_version, log=_LOGGER)
if not ISY.connected: if not ISY.connected:
return False return False
_categorize_nodes(hidden_identifier, sensor_identifier)
_categorize_programs()
# Listen for HA stop to disconnect. # Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load platforms for the devices in the ISY controller that we support. # Load platforms for the devices in the ISY controller that we support.
for component in ('sensor', 'light', 'switch'): for component in SUPPORTED_DOMAINS:
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
ISY.auto_update = True ISY.auto_update = True
return True return True
def stop(event): # pylint: disable=unused-argument
"""Cleanup the ISY subscription.""" def stop(event: object) -> None:
"""Stop ISY auto updates."""
ISY.auto_update = False ISY.auto_update = False
class ISYDeviceABC(ToggleEntity): class ISYDevice(Entity):
"""An abstract Class for an ISY device.""" """Representation of an ISY994 device."""
_attrs = {} _attrs = {}
_onattrs = [] _domain = None # type: str
_states = [] _name = None # type: str
_dtype = None
_domain = None
_name = None
def __init__(self, node): def __init__(self, node) -> None:
"""Initialize the device.""" """Initialize the insteon device."""
# setup properties self._node = node
self.node = node
# track changes self._change_handler = self._node.status.subscribe('changed',
self._change_handler = self.node.status. \ self.on_update)
subscribe('changed', self.on_update)
def __del__(self): def __del__(self) -> None:
"""Cleanup subscriptions because it is the right thing to do.""" """Cleanup the subscriptions."""
self._change_handler.unsubscribe() self._change_handler.unsubscribe()
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node."""
self.update_ha_state()
@property @property
def domain(self): def domain(self) -> str:
"""Return the domain of the entity.""" """Get the domain of the device."""
return self._domain return self._domain
@property @property
def dtype(self): def unique_id(self) -> str:
"""Return the data type of the entity (binary or analog).""" """Get the unique identifier of the device."""
if self._dtype in ['analog', 'binary']:
return self._dtype
return 'binary' if self.unit_of_measurement is None else 'analog'
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def value(self):
"""Return the unclean value from the controller."""
# pylint: disable=protected-access # pylint: disable=protected-access
return self.node.status._val return self._node._id
@property @property
def state_attributes(self): def raw_name(self) -> str:
"""Return the state attributes for the node.""" """Get the raw name of the device."""
attr = {}
for name, prop in self._attrs.items():
attr[name] = getattr(self, prop)
attr = self._attr_filter(attr)
return attr
def _attr_filter(self, attr):
"""A Placeholder for attribute filters."""
# pylint: disable=no-self-use
return attr
@property
def unique_id(self):
"""Return the ID of this ISY sensor."""
# pylint: disable=protected-access
return self.node._id
@property
def raw_name(self):
"""Return the unclean node name."""
return str(self._name) \ return str(self._name) \
if self._name is not None else str(self.node.name) if self._name is not None else str(self._node.name)
@property @property
def name(self): def name(self) -> str:
"""Return the cleaned name of the node.""" """Get the name of the device."""
return self.raw_name.replace(HIDDEN_STRING, '').strip() \ return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ') .replace('_', ' ')
@property @property
def hidden(self): def should_poll(self) -> bool:
"""Suggestion if the entity should be hidden from UIs.""" """No polling required since we're using the subscription."""
return False
@property
def value(self) -> object:
"""Get the current value of the device."""
# pylint: disable=protected-access
return self._node.status._val
@property
def state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
attr = {}
if hasattr(self._node, 'aux_properties'):
for name, val in self._node.aux_properties.items():
attr[name] = '{} {}'.format(val.get('value'), val.get('uom'))
return attr
@property
def hidden(self) -> bool:
"""Get whether the device should be hidden from the UI."""
return HIDDEN_STRING in self.raw_name return HIDDEN_STRING in self.raw_name
def update(self):
"""Update state of the sensor."""
# ISY objects are automatically updated by the ISY's event stream
pass
def on_update(self, event):
"""Handle the update received event."""
self.update_ha_state()
@property @property
def is_on(self): def unit_of_measurement(self) -> str:
"""Return a boolean response if the node is on.""" """Get the device unit of measure."""
return bool(self.value)
@property
def is_open(self):
"""Return boolean response if the node is open. On = Open."""
return self.is_on
@property
def state(self):
"""Return the state of the node."""
if len(self._states) > 0:
return self._states[0] if self.is_on else self._states[1]
return self.value
def turn_on(self, **kwargs):
"""Turn the device on."""
if self.domain is not 'sensor':
attrs = [kwargs.get(name) for name in self._onattrs]
self.node.on(*attrs)
else:
_LOGGER.error('ISY cannot turn on sensors.')
def turn_off(self, **kwargs):
"""Turn the device off."""
if self.domain is not 'sensor':
self.node.off()
else:
_LOGGER.error('ISY cannot turn off sensors.')
@property
def unit_of_measurement(self):
"""Return the defined units of measurement or None."""
try:
return self.node.units
except AttributeError:
return None return None
def _attr_filter(self, attr: str) -> str:
"""Filter the attribute."""
# pylint: disable=no-self-use
return attr
def update(self) -> None:
"""Perform an update for the device."""
pass

View File

@ -2,58 +2,68 @@
Support for ISY994 lights. Support for ISY994 lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/isy994/ https://home-assistant.io/components/light.isy994/
""" """
import logging import logging
from typing import Callable
from homeassistant.components.isy994 import ( from homeassistant.components.light import Light
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC) import homeassistant.components.isy994 as isy
from homeassistant.components.light import (ATTR_BRIGHTNESS, from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN
ATTR_SUPPORTED_FEATURES, from homeassistant.helpers.typing import ConfigType
SUPPORT_BRIGHTNESS)
from homeassistant.const import STATE_OFF, STATE_ON
SUPPORT_ISY994 = SUPPORT_BRIGHTNESS _LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=unused-argument
"""Setup the ISY994 platform.""" def setup_platform(hass, config: ConfigType,
logger = logging.getLogger(__name__) add_devices: Callable[[list], None], discovery_info=None):
devs = [] """Set up the ISY994 light platform."""
if isy.ISY is None or not isy.ISY.connected:
if ISY is None or not ISY.connected: _LOGGER.error('A connection has not been made to the ISY controller.')
logger.error('A connection has not been made to the ISY controller.')
return False return False
# Import dimmable nodes devices = []
for (path, node) in ISY.nodes:
if node.dimmable and SENSOR_STRING not in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYLightDevice(node))
add_devices(devs) for node in isy.filter_nodes(isy.NODES, units=UOM,
states=STATES):
if node.dimmable:
devices.append(ISYLightDevice(node))
add_devices(devices)
class ISYLightDevice(ISYDeviceABC): class ISYLightDevice(isy.ISYDevice, Light):
"""Representation of a ISY light.""" """Representation of an ISY994 light devie."""
_domain = 'light' def __init__(self, node: object) -> None:
_dtype = 'analog' """Initialize the ISY994 light device."""
_attrs = { isy.ISYDevice.__init__(self, node)
ATTR_BRIGHTNESS: 'value',
ATTR_SUPPORTED_FEATURES: 'supported_features',
}
_onattrs = [ATTR_BRIGHTNESS]
_states = [STATE_ON, STATE_OFF]
@property @property
def supported_features(self): def is_on(self) -> bool:
"""Flag supported features.""" """Get whether the ISY994 light is on."""
return SUPPORT_ISY994 return self.state == STATE_ON
def _attr_filter(self, attr): @property
"""Filter brightness out of entity while off.""" def state(self) -> str:
if ATTR_BRIGHTNESS in attr and not self.is_on: """Get the state of the ISY994 light."""
del attr[ATTR_BRIGHTNESS] return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
return attr
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device."""
if not self._node.fastOff():
_LOGGER.debug('Unable to turn on light.')
def turn_on(self, brightness=100, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
if not self._node.on(val=brightness):
_LOGGER.debug('Unable to turn on light.')

View File

@ -0,0 +1,123 @@
"""
Support for ISY994 locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.isy994/
"""
import logging
from typing import Callable # noqa
from homeassistant.components.lock import LockDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
0: STATE_UNLOCKED,
100: STATE_LOCKED
}
UOM = ['11']
STATES = [STATE_LOCKED, STATE_UNLOCKED]
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 lock platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
devices = []
for node in isy.filter_nodes(isy.NODES, units=UOM,
states=STATES):
devices.append(ISYLockDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
except (KeyError, AssertionError):
pass
else:
devices.append(ISYLockProgram(program.name, status, actions))
add_devices(devices)
class ISYLockDevice(isy.ISYDevice, LockDevice):
"""Representation of an ISY994 lock device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 lock device."""
isy.ISYDevice.__init__(self, node)
self._conn = node.parent.parent.conn
@property
def is_locked(self) -> bool:
"""Get whether the lock is in locked state."""
return self.state == STATE_LOCKED
@property
def state(self) -> str:
"""Get the state of the lock."""
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device."""
# Hack until PyISY is updated
req_url = self._conn.compileURL(['nodes', self.unique_id, 'cmd',
'SECMD', '1'])
response = self._conn.request(req_url)
if response is None:
_LOGGER.error('Unable to lock device')
self._node.update(0.5)
def unlock(self, **kwargs) -> None:
"""Send the unlock command to the ISY994 device."""
# Hack until PyISY is updated
req_url = self._conn.compileURL(['nodes', self.unique_id, 'cmd',
'SECMD', '0'])
response = self._conn.request(req_url)
if response is None:
_LOGGER.error('Unable to lock device')
self._node.update(0.5)
class ISYLockProgram(ISYLockDevice):
"""Representation of a ISY lock program."""
def __init__(self, name: str, node, actions) -> None:
"""Initialize the lock."""
ISYLockDevice.__init__(self, node)
self._name = name
self._actions = actions
@property
def is_locked(self) -> bool:
"""Return true if the device is locked."""
return bool(self.value)
@property
def state(self) -> str:
"""Return the state of the lock."""
return STATE_LOCKED if self.is_locked else STATE_UNLOCKED
def lock(self, **kwargs) -> None:
"""Lock the device."""
if not self._actions.runThen():
_LOGGER.error('Unable to lock device')
def unlock(self, **kwargs) -> None:
"""Unlock the device."""
if not self._actions.runElse():
_LOGGER.error('Unable to unlock device')

View File

@ -1,95 +1,311 @@
""" """
Support for ISY994 sensors. Support for ISY994 binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/isy994/ https://home-assistant.io/components/binary_sensor.isy994/
""" """
import logging import logging
from typing import Callable # noqa
from homeassistant.components.isy994 import ( import homeassistant.components.isy994 as isy
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC) from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF,
from homeassistant.const import ( STATE_ON)
STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN) from homeassistant.helpers.typing import ConfigType
DEFAULT_HIDDEN_WEATHER = ['Temperature_High', 'Temperature_Low', 'Feels_Like', _LOGGER = logging.getLogger(__name__)
'Temperature_Average', 'Pressure', 'Dew_Point',
'Gust_Speed', 'Evapotranspiration', UOM_FRIENDLY_NAME = {
'Irrigation_Requirement', 'Water_Deficit_Yesterday', '1': 'amp',
'Elevation', 'Average_Temperature_Tomorrow', '3': 'btu/h',
'High_Temperature_Tomorrow', '4': TEMP_CELSIUS,
'Low_Temperature_Tomorrow', 'Humidity_Tomorrow', '5': 'cm',
'Wind_Speed_Tomorrow', 'Gust_Speed_Tomorrow', '6': 'ft³',
'Rain_Tomorrow', 'Snow_Tomorrow', '7': 'ft³/min',
'Forecast_Average_Temperature', '8': '',
'Forecast_High_Temperature', '9': 'day',
'Forecast_Low_Temperature', 'Forecast_Humidity', '10': 'days',
'Forecast_Rain', 'Forecast_Snow'] '12': 'dB',
'13': 'dB A',
'14': '°',
'16': 'macroseismic',
'17': TEMP_FAHRENHEIT,
'18': 'ft',
'19': 'hour',
'20': 'hours',
'21': 'abs. humidity (%)',
'22': 'rel. humidity (%)',
'23': 'inHg',
'24': 'in/hr',
'25': 'index',
'26': 'K',
'27': 'keyword',
'28': 'kg',
'29': 'kV',
'30': 'kW',
'31': 'kPa',
'32': 'KPH',
'33': 'kWH',
'34': 'liedu',
'35': 'l',
'36': 'lux',
'37': 'mercalli',
'38': 'm',
'39': 'm³/hr',
'40': 'm/s',
'41': 'mA',
'42': 'ms',
'43': 'mV',
'44': 'min',
'45': 'min',
'46': 'mm/hr',
'47': 'month',
'48': 'MPH',
'49': 'm/s',
'50': 'ohm',
'51': '%',
'52': 'lb',
'53': 'power factor',
'54': 'ppm',
'55': 'pulse count',
'57': 's',
'58': 's',
'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': 'V',
'73': 'W',
'74': 'W/m²',
'75': 'weekday',
'76': 'Wind Direction (°)',
'77': 'year',
'82': 'mm',
'83': 'km',
'85': 'ohm',
'86': 'kOhm',
'87': 'm³/m³',
'88': 'Water activity',
'89': 'RPM',
'90': 'Hz',
'91': '° (Relative to North)',
'92': '° (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'
}
}
BINARY_UOM = ['2', '78']
def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=unused-argument
"""Setup the ISY994 platform.""" def setup_platform(hass, config: ConfigType,
# pylint: disable=protected-access add_devices: Callable[[list], None], discovery_info=None):
logger = logging.getLogger(__name__) """Setup the ISY994 sensor platform."""
devs = [] if isy.ISY is None or not isy.ISY.connected:
# Verify connection _LOGGER.error('A connection has not been made to the ISY controller.')
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False return False
# Import weather devices = []
if ISY.climate is not None:
for prop in ISY.climate._id2name:
if prop is not None:
prefix = HIDDEN_STRING \
if prop in DEFAULT_HIDDEN_WEATHER else ''
node = WeatherPseudoNode('ISY.weather.' + prop, prefix + prop,
getattr(ISY.climate, prop),
getattr(ISY.climate, prop + '_units'))
devs.append(ISYSensorDevice(node))
# Import sensor nodes for node in isy.SENSOR_NODES:
for (path, node) in ISY.nodes: if (len(node.uom) == 0 or node.uom[0] not in BINARY_UOM) and \
if SENSOR_STRING in node.name: STATE_OFF not in node.uom and STATE_ON not in node.uom:
if HIDDEN_STRING in path: _LOGGER.debug('LOADING %s', node.name)
node.name += HIDDEN_STRING devices.append(ISYSensorDevice(node))
devs.append(ISYSensorDevice(node, [STATE_ON, STATE_OFF]))
# Import sensor programs add_devices(devices)
for (folder_name, states) in (
('HA.locations', [STATE_HOME, STATE_NOT_HOME]),
('HA.sensors', [STATE_OPEN, STATE_CLOSED]), class ISYSensorDevice(isy.ISYDevice):
('HA.states', [STATE_ON, STATE_OFF])): """Representation of an ISY994 sensor device."""
try:
folder = ISY.programs['My Programs'][folder_name] def __init__(self, node) -> None:
except KeyError: """Initialize the ISY994 sensor device."""
# folder does not exist isy.ISYDevice.__init__(self, node)
pass
@property
def raw_unit_of_measurement(self) -> str:
"""Get the raw unit of measurement for the ISY994 sensor device."""
if len(self._node.uom) == 1:
if self._node.uom[0] in UOM_FRIENDLY_NAME:
friendly_name = UOM_FRIENDLY_NAME.get(self._node.uom[0])
if friendly_name == TEMP_CELSIUS or \
friendly_name == TEMP_FAHRENHEIT:
friendly_name = self.hass.config.units.temperature_unit
return friendly_name
else: else:
for _, _, node_id in folder.children: return self._node.uom[0]
node = folder[node_id].leaf else:
devs.append(ISYSensorDevice(node, states)) return None
add_devices(devs) @property
def state(self) -> str:
"""Get the state of the ISY994 sensor device."""
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)
elif self._node.prec and self._node.prec != [0]:
str_val = str(self.value)
int_prec = int(self._node.prec)
decimal_part = str_val[-int_prec:]
whole_part = str_val[:len(str_val) - int_prec]
val = float('{}.{}'.format(whole_part, decimal_part))
raw_units = self.raw_unit_of_measurement
if raw_units in (
TEMP_CELSIUS, TEMP_FAHRENHEIT):
val = self.hass.config.units.temperature(val, raw_units)
return str(val)
else:
return self.value
class WeatherPseudoNode(object): return None
"""This class allows weather variable to act as regular nodes."""
# pylint: disable=too-few-public-methods @property
def __init__(self, device_id, name, status, units=None): def unit_of_measurement(self) -> str:
"""Initialize the sensor.""" """Get the unit of measurement for the ISY994 sensor device."""
self._id = device_id raw_units = self.raw_unit_of_measurement
self.name = name if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
self.status = status return self.hass.config.units.temperature_unit
self.units = units else:
return raw_units
class ISYSensorDevice(ISYDeviceABC):
"""Representation of an ISY sensor."""
_domain = 'sensor'
def __init__(self, node, states=None):
"""Initialize the device."""
super().__init__(node)
self._states = states or []

View File

@ -2,87 +2,106 @@
Support for ISY994 switches. Support for ISY994 switches.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/isy994/ https://home-assistant.io/components/switch.isy994/
""" """
import logging import logging
from typing import Callable # noqa
from homeassistant.components.isy994 import ( from homeassistant.components.switch import SwitchDevice, DOMAIN
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC) import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_OFF, STATE_ON # STATE_OPEN, STATE_CLOSED from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType # noqa
_LOGGER = logging.getLogger(__name__)
VALUE_TO_STATE = {
False: STATE_OFF,
True: STATE_ON,
}
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
# The frontend doesn't seem to fully support the open and closed states yet. # pylint: disable=unused-argument
# Once it does, the HA.doors programs should report open and closed instead of def setup_platform(hass, config: ConfigType,
# off and on. It appears that on should be open and off should be closed. add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 switch platform."""
if isy.ISY is None or not isy.ISY.connected:
def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('A connection has not been made to the ISY controller.')
"""Setup the ISY994 platform."""
# pylint: disable=too-many-locals
logger = logging.getLogger(__name__)
devs = []
# verify connection
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
return False return False
# Import not dimmable nodes and groups devices = []
for (path, node) in ISY.nodes:
if not node.dimmable and SENSOR_STRING not in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYSwitchDevice(node))
# Import ISY doors programs for node in isy.filter_nodes(isy.NODES, units=UOM,
for folder_name, states in (('HA.doors', [STATE_ON, STATE_OFF]), states=STATES):
('HA.switches', [STATE_ON, STATE_OFF])): if not node.dimmable:
devices.append(ISYSwitchDevice(node))
for node in isy.GROUPS:
devices.append(ISYSwitchDevice(node))
for program in isy.PROGRAMS.get(DOMAIN, []):
try: try:
folder = ISY.programs['My Programs'][folder_name] status = program[isy.KEY_STATUS]
except KeyError: actions = program[isy.KEY_ACTIONS]
# HA.doors folder does not exist
pass
else:
for dtype, name, node_id in folder.children:
if dtype is 'folder':
custom_switch = folder[node_id]
try:
actions = custom_switch['actions'].leaf
assert actions.dtype == 'program', 'Not a program' assert actions.dtype == 'program', 'Not a program'
node = custom_switch['status'].leaf
except (KeyError, AssertionError): except (KeyError, AssertionError):
pass pass
else: else:
devs.append(ISYProgramDevice(name, node, actions, devices.append(ISYSwitchProgram(program.name, status, actions))
states))
add_devices(devs) add_devices(devices)
class ISYSwitchDevice(ISYDeviceABC): class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
"""Representation of an ISY switch.""" """Representation of an ISY994 switch device."""
_domain = 'switch' def __init__(self, node) -> None:
_dtype = 'binary' """Initialize the ISY994 switch device."""
_states = [STATE_ON, STATE_OFF] isy.ISYDevice.__init__(self, node)
@property
def is_on(self) -> bool:
"""Get whether the ISY994 device is in the on state."""
return self.state == STATE_ON
@property
def state(self) -> str:
"""Get the state of the ISY994 device."""
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
def turn_off(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch."""
if not self._node.off():
_LOGGER.debug('Unable to turn on switch.')
def turn_on(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 switch."""
if not self._node.on():
_LOGGER.debug('Unable to turn on switch.')
class ISYProgramDevice(ISYSwitchDevice): class ISYSwitchProgram(ISYSwitchDevice):
"""Representation of an ISY door.""" """A representation of an ISY994 program switch."""
_domain = 'switch' def __init__(self, name: str, node, actions) -> None:
_dtype = 'binary' """Initialize the ISY994 switch program."""
ISYSwitchDevice.__init__(self, node)
def __init__(self, name, node, actions, states):
"""Initialize the switch."""
super().__init__(node)
self._states = states
self._name = name self._name = name
self.action_node = actions self._actions = actions
def turn_on(self, **kwargs): @property
"""Turn the device on/close the device.""" def is_on(self) -> bool:
self.action_node.runThen() """Get whether the ISY994 switch program is on."""
return bool(self.value)
def turn_off(self, **kwargs): def turn_on(self, **kwargs) -> None:
"""Turn the device off/open the device.""" """Send the turn on command to the ISY994 switch program."""
self.action_node.runElse() if not self._actions.runThen():
_LOGGER.error('Unable to turn on switch')
def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 switch program."""
if not self._actions.runElse():
_LOGGER.error('Unable to turn off switch')

View File

@ -8,7 +8,7 @@ voluptuous==0.9.2
typing>=3,<4 typing>=3,<4
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.0.6 PyISY==1.0.7
# homeassistant.components.notify.html5 # homeassistant.components.notify.html5
PyJWT==1.4.2 PyJWT==1.4.2