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:
parent
8c7a1b4b05
commit
05a3b610ff
9 changed files with 1057 additions and 321 deletions
76
homeassistant/components/binary_sensor/isy994.py
Normal file
76
homeassistant/components/binary_sensor/isy994.py
Normal 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)
|
109
homeassistant/components/cover/isy994.py
Normal file
109
homeassistant/components/cover/isy994.py
Normal 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')
|
120
homeassistant/components/fan/isy994.py
Normal file
120
homeassistant/components/fan/isy994.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
|
123
homeassistant/components/lock/isy994.py
Normal file
123
homeassistant/components/lock/isy994.py
Normal 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')
|
|
@ -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': 'm³',
|
||||
'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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue