Envisalink support (#2304)
* Created a new platform for envisalink-based alarm panels (Honeywell/DSC) * Added a sensor component and cleanup * Completed initial development. * Fixing pylint issues. * Fix more pylint issues * Fixed more validation issues. * Final pylint issues * Final tweaks prior to PR. * Fixed final pylint issue * Resolved a few minor issues, and used volumptous for validation. * Fixing final lint issues * Fixes to validation schema and refactoring.pull/2218/merge
parent
44177a7fde
commit
cb6f50b7ff
|
@ -20,6 +20,9 @@ omit =
|
|||
homeassistant/components/ecobee.py
|
||||
homeassistant/components/*/ecobee.py
|
||||
|
||||
homeassistant/components/envisalink.py
|
||||
homeassistant/components/*/envisalink.py
|
||||
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
"""
|
||||
Support for Envisalink-based alarm control panels (Honeywell/DSC).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.envisalink/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
_code = discovery_info[CONF_CODE]
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkAlarm(
|
||||
part_num,
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
_code,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Represents the Envisalink-based alarm panel."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, partition_number, alarm_name, code, info, controller):
|
||||
"""Initialize the alarm panel."""
|
||||
from pydispatch import dispatcher
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
_LOGGER.debug('Setting up alarm: ' + alarm_name)
|
||||
EnvisalinkDevice.__init__(self, alarm_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update HA state, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._info['status']['alarm']:
|
||||
return STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['alpha']:
|
||||
return STATE_ALARM_DISARMED
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.disarm_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_stay_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if self._code:
|
||||
EVL_CONTROLLER.arm_away_partition(str(code),
|
||||
self._partition_number)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command. Not possible for us."""
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,71 @@
|
|||
"""
|
||||
Support for Envisalink zone states- represented as binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink sensor devices."""
|
||||
_configured_zones = discovery_info['zones']
|
||||
for zone_num in _configured_zones:
|
||||
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
|
||||
_device = EnvisalinkBinarySensor(
|
||||
zone_num,
|
||||
_device_config_data[CONF_ZONENAME],
|
||||
_device_config_data[CONF_ZONETYPE],
|
||||
EVL_CONTROLLER.alarm_state['zone'][zone_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an envisalink Binary Sensor."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: ' + zone_name)
|
||||
EnvisalinkDevice.__init__(self, zone_name, info, controller)
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
|
||||
return attr
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info['status']['open']
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
def _update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
if zone is None or int(zone) == self._zone_number:
|
||||
self.update_ha_state()
|
|
@ -0,0 +1,210 @@
|
|||
"""
|
||||
Support for Envisalink devices.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/envisalink/
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['pyenvisalink==0.9', 'pydispatcher==2.0.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = 'envisalink'
|
||||
|
||||
EVL_CONTROLLER = None
|
||||
|
||||
CONF_EVL_HOST = 'host'
|
||||
CONF_EVL_PORT = 'port'
|
||||
CONF_PANEL_TYPE = 'panel_type'
|
||||
CONF_EVL_VERSION = 'evl_version'
|
||||
CONF_CODE = 'code'
|
||||
CONF_USERNAME = 'user_name'
|
||||
CONF_PASS = 'password'
|
||||
CONF_EVL_KEEPALIVE = 'keepalive_interval'
|
||||
CONF_ZONEDUMP_INTERVAL = 'zonedump_interval'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_PARTITIONS = 'partitions'
|
||||
|
||||
CONF_ZONENAME = 'name'
|
||||
CONF_ZONETYPE = 'type'
|
||||
CONF_PARTITIONNAME = 'name'
|
||||
|
||||
DEFAULT_PORT = 4025
|
||||
DEFAULT_EVL_VERSION = 3
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
DEFAULT_ZONEDUMP_INTERVAL = 30
|
||||
DEFAULT_ZONETYPE = 'opening'
|
||||
|
||||
SIGNAL_ZONE_UPDATE = 'zones_updated'
|
||||
SIGNAL_PARTITION_UPDATE = 'partition_updated'
|
||||
SIGNAL_KEYPAD_UPDATE = 'keypad_updated'
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONENAME): cv.string,
|
||||
vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string})
|
||||
|
||||
PARTITION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PARTITIONNAME): cv.string})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_EVL_HOST): cv.string,
|
||||
vol.Required(CONF_PANEL_TYPE):
|
||||
vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASS): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA},
|
||||
vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
|
||||
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
vol.Optional(CONF_ZONEDUMP_INTERVAL,
|
||||
default=DEFAULT_ZONEDUMP_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=15)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument, too-many-function-args, too-many-locals
|
||||
# pylint: disable=too-many-return-statements
|
||||
def setup(hass, base_config):
|
||||
"""Common setup for Envisalink devices."""
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
from pydispatch import dispatcher
|
||||
|
||||
global EVL_CONTROLLER
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
|
||||
_host = config.get(CONF_EVL_HOST)
|
||||
_port = config.get(CONF_EVL_PORT)
|
||||
_code = config.get(CONF_CODE)
|
||||
_panel_type = config.get(CONF_PANEL_TYPE)
|
||||
_version = config.get(CONF_EVL_VERSION)
|
||||
_user = config.get(CONF_USERNAME)
|
||||
_pass = config.get(CONF_PASS)
|
||||
_keep_alive = config.get(CONF_EVL_KEEPALIVE)
|
||||
_zone_dump = config.get(CONF_ZONEDUMP_INTERVAL)
|
||||
_zones = config.get(CONF_ZONES)
|
||||
_partitions = config.get(CONF_PARTITIONS)
|
||||
_connect_status = {}
|
||||
EVL_CONTROLLER = EnvisalinkAlarmPanel(_host,
|
||||
_port,
|
||||
_panel_type,
|
||||
_version,
|
||||
_user,
|
||||
_pass,
|
||||
_zone_dump,
|
||||
_keep_alive)
|
||||
|
||||
def login_fail_callback(data):
|
||||
"""Callback for when the evl rejects our login."""
|
||||
_LOGGER.error("The envisalink rejected your credentials.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_fail_callback(data):
|
||||
"""Network failure callback."""
|
||||
_LOGGER.error("Could not establish a connection with the envisalink.")
|
||||
_connect_status['fail'] = 1
|
||||
|
||||
def connection_success_callback(data):
|
||||
"""Callback for a successful connection."""
|
||||
_LOGGER.info("Established a connection with the envisalink.")
|
||||
_connect_status['success'] = 1
|
||||
|
||||
def zones_updated_callback(data):
|
||||
"""Handle zone timer updates."""
|
||||
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
|
||||
dispatcher.send(signal=SIGNAL_ZONE_UPDATE,
|
||||
sender=None,
|
||||
zone=data)
|
||||
|
||||
def alarm_data_updated_callback(data):
|
||||
"""Handle non-alarm based info updates."""
|
||||
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
|
||||
dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def partition_updated_callback(data):
|
||||
"""Handle partition changes thrown by evl (including alarms)."""
|
||||
_LOGGER.info("The envisalink sent a partition update event.")
|
||||
dispatcher.send(signal=SIGNAL_PARTITION_UPDATE,
|
||||
sender=None,
|
||||
partition=data)
|
||||
|
||||
def stop_envisalink(event):
|
||||
"""Shutdown envisalink connection and thread on exit."""
|
||||
_LOGGER.info("Shutting down envisalink.")
|
||||
EVL_CONTROLLER.stop()
|
||||
|
||||
def start_envisalink(event):
|
||||
"""Startup process for the envisalink."""
|
||||
EVL_CONTROLLER.start()
|
||||
for _ in range(10):
|
||||
if 'success' in _connect_status:
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink)
|
||||
return True
|
||||
elif 'fail' in _connect_status:
|
||||
return False
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
_LOGGER.error("Timeout occurred while establishing evl connection.")
|
||||
return False
|
||||
|
||||
EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback
|
||||
EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback
|
||||
EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback
|
||||
EVL_CONTROLLER.callback_login_failure = login_fail_callback
|
||||
EVL_CONTROLLER.callback_login_timeout = connection_fail_callback
|
||||
EVL_CONTROLLER.callback_login_success = connection_success_callback
|
||||
|
||||
_result = start_envisalink(None)
|
||||
if not _result:
|
||||
return False
|
||||
|
||||
# Load sub-components for envisalink
|
||||
if _partitions:
|
||||
load_platform(hass, 'alarm_control_panel', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
load_platform(hass, 'sensor', 'envisalink',
|
||||
{'partitions': _partitions,
|
||||
'code': _code}, config)
|
||||
if _zones:
|
||||
load_platform(hass, 'binary_sensor', 'envisalink',
|
||||
{'zones': _zones}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EnvisalinkDevice(Entity):
|
||||
"""Representation of an envisalink devicetity."""
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
"""Initialize the device."""
|
||||
self._controller = controller
|
||||
self._info = info
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
|
@ -0,0 +1,68 @@
|
|||
"""
|
||||
Support for Envisalink sensors (shows panel info).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.envisalink/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components.envisalink import (EVL_CONTROLLER,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_PARTITIONNAME,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_KEYPAD_UPDATE)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Perform the setup for Envisalink sensor devices."""
|
||||
_configured_partitions = discovery_info['partitions']
|
||||
for part_num in _configured_partitions:
|
||||
_device_config_data = PARTITION_SCHEMA(
|
||||
_configured_partitions[part_num])
|
||||
_device = EnvisalinkSensor(
|
||||
_device_config_data[CONF_PARTITIONNAME],
|
||||
part_num,
|
||||
EVL_CONTROLLER.alarm_state['partition'][part_num],
|
||||
EVL_CONTROLLER)
|
||||
add_devices_callback([_device])
|
||||
|
||||
|
||||
class EnvisalinkSensor(EnvisalinkDevice):
|
||||
"""Representation of an envisalink keypad."""
|
||||
|
||||
def __init__(self, partition_name, partition_number, info, controller):
|
||||
"""Initialize the sensor."""
|
||||
from pydispatch import dispatcher
|
||||
self._icon = 'mdi:alarm'
|
||||
self._partition_number = partition_number
|
||||
_LOGGER.debug('Setting up sensor for partition: ' + partition_name)
|
||||
EnvisalinkDevice.__init__(self,
|
||||
partition_name + ' Keypad',
|
||||
info,
|
||||
controller)
|
||||
|
||||
dispatcher.connect(self._update_callback,
|
||||
signal=SIGNAL_KEYPAD_UPDATE,
|
||||
sender=dispatcher.Any)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the overall state."""
|
||||
return self._info['status']['alpha']
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._info['status']
|
||||
|
||||
def _update_callback(self, partition):
|
||||
"""Update the partition state in HA, if needed."""
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.update_ha_state()
|
|
@ -241,9 +241,13 @@ pyasn1==0.1.9
|
|||
# homeassistant.components.media_player.cast
|
||||
pychromecast==0.7.2
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
# homeassistant.components.zwave
|
||||
pydispatcher==2.0.5
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==0.9
|
||||
|
||||
# homeassistant.components.ifttt
|
||||
pyfttt==0.3
|
||||
|
||||
|
|
Loading…
Reference in New Issue