From cb6f50b7ffe80bcfbeda2efdd45cebaa4a22c883 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon <Cinntax@users.noreply.github.com> Date: Sun, 19 Jun 2016 12:45:07 -0500 Subject: [PATCH] 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. --- .coveragerc | 3 + .../alarm_control_panel/envisalink.py | 105 +++++++++ .../components/binary_sensor/envisalink.py | 71 ++++++ homeassistant/components/envisalink.py | 210 ++++++++++++++++++ homeassistant/components/sensor/envisalink.py | 68 ++++++ requirements_all.txt | 4 + 6 files changed, 461 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/envisalink.py create mode 100644 homeassistant/components/binary_sensor/envisalink.py create mode 100644 homeassistant/components/envisalink.py create mode 100644 homeassistant/components/sensor/envisalink.py diff --git a/.coveragerc b/.coveragerc index e8666da60f4..265c653d636 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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 diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py new file mode 100644 index 00000000000..ebd54da1558 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/envisalink.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() diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py new file mode 100644 index 00000000000..144de83aa53 --- /dev/null +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -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() diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py new file mode 100644 index 00000000000..f1a7009e059 --- /dev/null +++ b/homeassistant/components/envisalink.py @@ -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 diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/sensor/envisalink.py new file mode 100644 index 00000000000..cd71673b99f --- /dev/null +++ b/homeassistant/components/sensor/envisalink.py @@ -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() diff --git a/requirements_all.txt b/requirements_all.txt index 368ea27649c..bafb405b956 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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