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
from urllib.parse import urlparse
import voluptuous as vol
from homeassistant.core import HomeAssistant # noqa
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config, discovery
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.6']
REQUIREMENTS = ['PyISY==1.0.7']
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
DEFAULT_SENSOR_STRING = 'sensor'
DEFAULT_HIDDEN_STRING = '{HIDE ME}'
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__)
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):
"""Setup ISY994 component.
SENSOR_NODES = []
NODES = []
GROUPS = []
PROGRAMS = {}
This will automatically import associated lights, switches, and sensors.
"""
import PyISY
PYISY = None
# pylint: disable=global-statement
# check for required values in configuration file
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return False
HIDDEN_STRING = DEFAULT_HIDDEN_STRING
# Pull and parse standard configuration.
user = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
host = urlparse(config[DOMAIN][CONF_HOST])
SUPPORTED_DOMAINS = ['binary_sensor', 'cover', 'fan', 'light', 'lock',
'sensor', 'switch']
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()
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':
addr = addr.replace('http://', '')
https = False
@ -50,169 +157,125 @@ def setup(hass, config):
addr = addr.replace('https://', '')
https = True
else:
_LOGGER.error('isy994 host value in configuration file is invalid.')
_LOGGER.error('isy994 host value in configuration is invalid.')
return False
port = host.port
addr = addr.replace(':{}'.format(port), '')
# Pull and parse optional configuration.
global SENSOR_STRING
global HIDDEN_STRING
SENSOR_STRING = str(config[DOMAIN].get('sensor_string', SENSOR_STRING))
HIDDEN_STRING = str(config[DOMAIN].get('hidden_string', HIDDEN_STRING))
tls_version = config[DOMAIN].get(CONF_TLS_VER, None)
import PyISY
global PYISY
PYISY = PyISY
# Connect to ISY controller.
global ISY
ISY = PyISY.ISY(addr, port, user, password, use_https=https,
tls_ver=tls_version, log=_LOGGER)
ISY = PyISY.ISY(addr, port, username=user, password=password,
use_https=https, tls_ver=tls_version, log=_LOGGER)
if not ISY.connected:
return False
_categorize_nodes(hidden_identifier, sensor_identifier)
_categorize_programs()
# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# 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)
ISY.auto_update = True
return True
def stop(event):
"""Cleanup the ISY subscription."""
# pylint: disable=unused-argument
def stop(event: object) -> None:
"""Stop ISY auto updates."""
ISY.auto_update = False
class ISYDeviceABC(ToggleEntity):
"""An abstract Class for an ISY device."""
class ISYDevice(Entity):
"""Representation of an ISY994 device."""
_attrs = {}
_onattrs = []
_states = []
_dtype = None
_domain = None
_name = None
_domain = None # type: str
_name = None # type: str
def __init__(self, node):
"""Initialize the device."""
# setup properties
self.node = node
def __init__(self, node) -> None:
"""Initialize the insteon device."""
self._node = node
# track changes
self._change_handler = self.node.status. \
subscribe('changed', self.on_update)
self._change_handler = self._node.status.subscribe('changed',
self.on_update)
def __del__(self):
"""Cleanup subscriptions because it is the right thing to do."""
def __del__(self) -> None:
"""Cleanup the subscriptions."""
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
def domain(self):
"""Return the domain of the entity."""
def domain(self) -> str:
"""Get the domain of the device."""
return self._domain
@property
def dtype(self):
"""Return the data type of the entity (binary or analog)."""
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."""
def unique_id(self) -> str:
"""Get the unique identifier of the device."""
# pylint: disable=protected-access
return self.node.status._val
return self._node._id
@property
def state_attributes(self):
"""Return the state attributes for the node."""
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."""
def raw_name(self) -> str:
"""Get the raw name of the device."""
return str(self._name) \
if self._name is not None else str(self.node.name)
if self._name is not None else str(self._node.name)
@property
def name(self):
"""Return the cleaned name of the node."""
def name(self) -> str:
"""Get the name of the device."""
return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ')
@property
def hidden(self):
"""Suggestion if the entity should be hidden from UIs."""
def should_poll(self) -> bool:
"""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
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
def is_on(self):
"""Return a boolean response if the node is on."""
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:
def unit_of_measurement(self) -> str:
"""Get the device unit of measure."""
return None
def _attr_filter(self, attr: str) -> str:
"""Filter the attribute."""
# pylint: disable=no-self-use
return attr
def update(self) -> None:
"""Perform an update for the device."""
pass

View file

@ -2,58 +2,68 @@
Support for ISY994 lights.
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
from typing import Callable
from homeassistant.components.isy994 import (
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC)
from homeassistant.components.light import (ATTR_BRIGHTNESS,
ATTR_SUPPORTED_FEATURES,
SUPPORT_BRIGHTNESS)
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.components.light import Light
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN
from homeassistant.helpers.typing import ConfigType
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):
"""Setup the ISY994 platform."""
logger = logging.getLogger(__name__)
devs = []
if ISY is None or not ISY.connected:
logger.error('A connection has not been made to the ISY controller.')
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 light platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
# Import dimmable nodes
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))
devices = []
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):
"""Representation of a ISY light."""
class ISYLightDevice(isy.ISYDevice, Light):
"""Representation of an ISY994 light devie."""
_domain = 'light'
_dtype = 'analog'
_attrs = {
ATTR_BRIGHTNESS: 'value',
ATTR_SUPPORTED_FEATURES: 'supported_features',
}
_onattrs = [ATTR_BRIGHTNESS]
_states = [STATE_ON, STATE_OFF]
def __init__(self, node: object) -> None:
"""Initialize the ISY994 light device."""
isy.ISYDevice.__init__(self, node)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_ISY994
def is_on(self) -> bool:
"""Get whether the ISY994 light is on."""
return self.state == STATE_ON
def _attr_filter(self, attr):
"""Filter brightness out of entity while off."""
if ATTR_BRIGHTNESS in attr and not self.is_on:
del attr[ATTR_BRIGHTNESS]
return attr
@property
def state(self) -> str:
"""Get the state of the ISY994 light."""
return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
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
https://home-assistant.io/components/isy994/
https://home-assistant.io/components/binary_sensor.isy994/
"""
import logging
from typing import Callable # noqa
from homeassistant.components.isy994 import (
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC)
from homeassistant.const import (
STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN)
import homeassistant.components.isy994 as isy
from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF,
STATE_ON)
from homeassistant.helpers.typing import ConfigType
DEFAULT_HIDDEN_WEATHER = ['Temperature_High', 'Temperature_Low', 'Feels_Like',
'Temperature_Average', 'Pressure', 'Dew_Point',
'Gust_Speed', 'Evapotranspiration',
'Irrigation_Requirement', 'Water_Deficit_Yesterday',
'Elevation', 'Average_Temperature_Tomorrow',
'High_Temperature_Tomorrow',
'Low_Temperature_Tomorrow', 'Humidity_Tomorrow',
'Wind_Speed_Tomorrow', 'Gust_Speed_Tomorrow',
'Rain_Tomorrow', 'Snow_Tomorrow',
'Forecast_Average_Temperature',
'Forecast_High_Temperature',
'Forecast_Low_Temperature', 'Forecast_Humidity',
'Forecast_Rain', 'Forecast_Snow']
_LOGGER = logging.getLogger(__name__)
UOM_FRIENDLY_NAME = {
'1': 'amp',
'3': 'btu/h',
'4': TEMP_CELSIUS,
'5': 'cm',
'6': 'ft³',
'7': 'ft³/min',
'8': '',
'9': 'day',
'10': 'days',
'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):
"""Setup the ISY994 platform."""
# pylint: disable=protected-access
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.')
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Setup the ISY994 sensor platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
# Import weather
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))
devices = []
# Import sensor nodes
for (path, node) in ISY.nodes:
if SENSOR_STRING in node.name:
if HIDDEN_STRING in path:
node.name += HIDDEN_STRING
devs.append(ISYSensorDevice(node, [STATE_ON, STATE_OFF]))
for node in isy.SENSOR_NODES:
if (len(node.uom) == 0 or node.uom[0] not in BINARY_UOM) and \
STATE_OFF not in node.uom and STATE_ON not in node.uom:
_LOGGER.debug('LOADING %s', node.name)
devices.append(ISYSensorDevice(node))
# Import sensor programs
for (folder_name, states) in (
('HA.locations', [STATE_HOME, STATE_NOT_HOME]),
('HA.sensors', [STATE_OPEN, STATE_CLOSED]),
('HA.states', [STATE_ON, STATE_OFF])):
try:
folder = ISY.programs['My Programs'][folder_name]
except KeyError:
# folder does not exist
pass
add_devices(devices)
class ISYSensorDevice(isy.ISYDevice):
"""Representation of an ISY994 sensor device."""
def __init__(self, node) -> None:
"""Initialize the ISY994 sensor device."""
isy.ISYDevice.__init__(self, node)
@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:
for _, _, node_id in folder.children:
node = folder[node_id].leaf
devs.append(ISYSensorDevice(node, states))
return self._node.uom[0]
else:
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):
"""This class allows weather variable to act as regular nodes."""
return None
# pylint: disable=too-few-public-methods
def __init__(self, device_id, name, status, units=None):
"""Initialize the sensor."""
self._id = device_id
self.name = name
self.status = status
self.units = 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 []
@property
def unit_of_measurement(self) -> str:
"""Get the unit of measurement for the ISY994 sensor device."""
raw_units = self.raw_unit_of_measurement
if raw_units in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
return self.hass.config.units.temperature_unit
else:
return raw_units

View file

@ -2,87 +2,106 @@
Support for ISY994 switches.
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
from typing import Callable # noqa
from homeassistant.components.isy994 import (
HIDDEN_STRING, ISY, SENSOR_STRING, ISYDeviceABC)
from homeassistant.const import STATE_OFF, STATE_ON # STATE_OPEN, STATE_CLOSED
from homeassistant.components.switch import SwitchDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN
from homeassistant.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.
# Once it does, the HA.doors programs should report open and closed instead of
# off and on. It appears that on should be open and off should be closed.
def setup_platform(hass, config, add_devices, discovery_info=None):
"""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.')
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 switch platform."""
if isy.ISY is None or not isy.ISY.connected:
_LOGGER.error('A connection has not been made to the ISY controller.')
return False
# Import not dimmable nodes and groups
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))
devices = []
# Import ISY doors programs
for folder_name, states in (('HA.doors', [STATE_ON, STATE_OFF]),
('HA.switches', [STATE_ON, STATE_OFF])):
for node in isy.filter_nodes(isy.NODES, units=UOM,
states=STATES):
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:
folder = ISY.programs['My Programs'][folder_name]
except KeyError:
# 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
status = program[isy.KEY_STATUS]
actions = program[isy.KEY_ACTIONS]
assert actions.dtype == 'program', 'Not a program'
node = custom_switch['status'].leaf
except (KeyError, AssertionError):
pass
else:
devs.append(ISYProgramDevice(name, node, actions,
states))
devices.append(ISYSwitchProgram(program.name, status, actions))
add_devices(devs)
add_devices(devices)
class ISYSwitchDevice(ISYDeviceABC):
"""Representation of an ISY switch."""
class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
"""Representation of an ISY994 switch device."""
_domain = 'switch'
_dtype = 'binary'
_states = [STATE_ON, STATE_OFF]
def __init__(self, node) -> None:
"""Initialize the ISY994 switch device."""
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):
"""Representation of an ISY door."""
class ISYSwitchProgram(ISYSwitchDevice):
"""A representation of an ISY994 program switch."""
_domain = 'switch'
_dtype = 'binary'
def __init__(self, name, node, actions, states):
"""Initialize the switch."""
super().__init__(node)
self._states = states
def __init__(self, name: str, node, actions) -> None:
"""Initialize the ISY994 switch program."""
ISYSwitchDevice.__init__(self, node)
self._name = name
self.action_node = actions
self._actions = actions
def turn_on(self, **kwargs):
"""Turn the device on/close the device."""
self.action_node.runThen()
@property
def is_on(self) -> bool:
"""Get whether the ISY994 switch program is on."""
return bool(self.value)
def turn_off(self, **kwargs):
"""Turn the device off/open the device."""
self.action_node.runElse()
def turn_on(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch program."""
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
# homeassistant.components.isy994
PyISY==1.0.6
PyISY==1.0.7
# homeassistant.components.notify.html5
PyJWT==1.4.2