From 3996c609b41abd8fdd7efbfef00762a94b0ddf2d Mon Sep 17 00:00:00 2001
From: c-soft <c-soft@users.noreply.github.com>
Date: Mon, 18 Sep 2017 17:42:31 +0200
Subject: [PATCH] Added satel_integra alarm panel and binary sensor platform
 (#9336)

* Added satel_integra alarm panel and binary sensor platform

* Fixed several issues after review: import cleanup, reduced messaging levels to debug, other.

* Fixes after review: removed dead code, improved loop, sorted imports.

* Changes after review, not yet working

* Changes after review - wrapped async code, killed ensure_future, moved async_load_platform into jobs
---
 .coveragerc                                   |   3 +
 .../alarm_control_panel/satel_integra.py      |  94 +++++++++++
 .../components/binary_sensor/satel_integra.py |  90 +++++++++++
 homeassistant/components/satel_integra.py     | 152 ++++++++++++++++++
 requirements_all.txt                          |   3 +
 5 files changed, 342 insertions(+)
 create mode 100644 homeassistant/components/alarm_control_panel/satel_integra.py
 create mode 100644 homeassistant/components/binary_sensor/satel_integra.py
 create mode 100644 homeassistant/components/satel_integra.py

diff --git a/.coveragerc b/.coveragerc
index a00599d7733..239c155d7ac 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -161,6 +161,9 @@ omit =
     homeassistant/components/rpi_pfio.py
     homeassistant/components/*/rpi_pfio.py
 
+    homeassistant/components/satel_integra.py
+    homeassistant/components/*/satel_integra.py
+
     homeassistant/components/scsgate.py
     homeassistant/components/*/scsgate.py
 
diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py
new file mode 100644
index 00000000000..6115311f873
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/satel_integra.py
@@ -0,0 +1,94 @@
+"""
+Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ .
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/alarm_control_panel.satel_integra/
+"""
+import asyncio
+import logging
+
+import homeassistant.components.alarm_control_panel as alarm
+from homeassistant.components.satel_integra import (CONF_ARM_HOME_MODE,
+                                                    DATA_SATEL,
+                                                    SIGNAL_PANEL_MESSAGE)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['satel_integra']
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+    """Set up for AlarmDecoder alarm panels."""
+    if not discovery_info:
+        return
+
+    device = SatelIntegraAlarmPanel("Alarm Panel",
+                                    discovery_info.get(CONF_ARM_HOME_MODE))
+    async_add_devices([device])
+
+
+class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
+    """Representation of an AlarmDecoder-based alarm panel."""
+
+    def __init__(self, name, arm_home_mode):
+        """Initialize the alarm panel."""
+        self._name = name
+        self._state = None
+        self._arm_home_mode = arm_home_mode
+
+    @asyncio.coroutine
+    def async_added_to_hass(self):
+        """Register callbacks."""
+        async_dispatcher_connect(
+            self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
+
+    @callback
+    def _message_callback(self, message):
+
+        if message != self._state:
+            self._state = message
+            self.async_schedule_update_ha_state()
+        else:
+            _LOGGER.warning("Ignoring alarm status message, same state")
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._name
+
+    @property
+    def should_poll(self):
+        """Return the polling state."""
+        return False
+
+    @property
+    def code_format(self):
+        """Return the regex for code format or None if no code is required."""
+        return '^\\d{4,6}$'
+
+    @property
+    def state(self):
+        """Return the state of the device."""
+        return self._state
+
+    @asyncio.coroutine
+    def async_alarm_disarm(self, code=None):
+        """Send disarm command."""
+        if code:
+            yield from self.hass.data[DATA_SATEL].disarm(code)
+
+    @asyncio.coroutine
+    def async_alarm_arm_away(self, code=None):
+        """Send arm away command."""
+        if code:
+            yield from self.hass.data[DATA_SATEL].arm(code)
+
+    @asyncio.coroutine
+    def async_alarm_arm_home(self, code=None):
+        """Send arm home command."""
+        if code:
+            yield from self.hass.data[DATA_SATEL].arm(code,
+                                                      self._arm_home_mode)
diff --git a/homeassistant/components/binary_sensor/satel_integra.py b/homeassistant/components/binary_sensor/satel_integra.py
new file mode 100644
index 00000000000..f373809f7c0
--- /dev/null
+++ b/homeassistant/components/binary_sensor/satel_integra.py
@@ -0,0 +1,90 @@
+"""
+Support for Satel Integra 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.satel_integra/
+"""
+import asyncio
+import logging
+
+from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.components.satel_integra import (CONF_ZONES,
+                                                    CONF_ZONE_NAME,
+                                                    CONF_ZONE_TYPE,
+                                                    SIGNAL_ZONES_UPDATED)
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+DEPENDENCIES = ['satel_integra']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+    """Set up the Satel Integra binary sensor devices."""
+    if not discovery_info:
+        return
+
+    configured_zones = discovery_info[CONF_ZONES]
+
+    devices = []
+
+    for zone_num, device_config_data in configured_zones.items():
+        zone_type = device_config_data[CONF_ZONE_TYPE]
+        zone_name = device_config_data[CONF_ZONE_NAME]
+        device = SatelIntegraBinarySensor(zone_num, zone_name, zone_type)
+        devices.append(device)
+
+    async_add_devices(devices)
+
+
+class SatelIntegraBinarySensor(BinarySensorDevice):
+    """Representation of an Satel Integra binary sensor."""
+
+    def __init__(self, zone_number, zone_name, zone_type):
+        """Initialize the binary_sensor."""
+        self._zone_number = zone_number
+        self._name = zone_name
+        self._zone_type = zone_type
+        self._state = 0
+
+    @asyncio.coroutine
+    def async_added_to_hass(self):
+        """Register callbacks."""
+        async_dispatcher_connect(
+            self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated)
+
+    @property
+    def name(self):
+        """Return the name of the entity."""
+        return self._name
+
+    @property
+    def icon(self):
+        """Icon for device by its type."""
+        if self._zone_type == 'smoke':
+            return "mdi:fire"
+
+    @property
+    def should_poll(self):
+        """No polling needed."""
+        return False
+
+    @property
+    def is_on(self):
+        """Return true if sensor is on."""
+        return self._state == 1
+
+    @property
+    def device_class(self):
+        """Return the class of this sensor, from DEVICE_CLASSES."""
+        return self._zone_type
+
+    @callback
+    def _zones_updated(self, zones):
+        """Update the zone's state, if needed."""
+        if self._zone_number in zones \
+                and self._state != zones[self._zone_number]:
+            self._state = zones[self._zone_number]
+            self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py
new file mode 100644
index 00000000000..4b61ff15c08
--- /dev/null
+++ b/homeassistant/components/satel_integra.py
@@ -0,0 +1,152 @@
+"""
+Support for Satel Integra devices.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/satel_integra/
+"""
+# pylint: disable=invalid-name
+
+import asyncio
+import logging
+
+
+import voluptuous as vol
+
+from homeassistant.core import callback
+from homeassistant.const import (
+    STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
+    STATE_ALARM_TRIGGERED, EVENT_HOMEASSISTANT_STOP)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+
+REQUIREMENTS = ['satel_integra==0.1.0']
+
+DEFAULT_ALARM_NAME = 'satel_integra'
+DEFAULT_PORT = 7094
+DEFAULT_CONF_ARM_HOME_MODE = 1
+DEFAULT_DEVICE_PARTITION = 1
+DEFAULT_ZONE_TYPE = 'motion'
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'satel_integra'
+
+DATA_SATEL = 'satel_integra'
+
+CONF_DEVICE_HOST = 'host'
+CONF_DEVICE_PORT = 'port'
+CONF_DEVICE_PARTITION = 'partition'
+CONF_ARM_HOME_MODE = 'arm_home_mode'
+CONF_ZONE_NAME = 'name'
+CONF_ZONE_TYPE = 'type'
+CONF_ZONES = 'zones'
+
+ZONES = 'zones'
+
+SIGNAL_PANEL_MESSAGE = 'satel_integra.panel_message'
+SIGNAL_PANEL_ARM_AWAY = 'satel_integra.panel_arm_away'
+SIGNAL_PANEL_ARM_HOME = 'satel_integra.panel_arm_home'
+SIGNAL_PANEL_DISARM = 'satel_integra.panel_disarm'
+
+SIGNAL_ZONES_UPDATED = 'satel_integra.zones_updated'
+
+ZONE_SCHEMA = vol.Schema({
+    vol.Required(CONF_ZONE_NAME): cv.string,
+    vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): cv.string})
+
+CONFIG_SCHEMA = vol.Schema({
+    DOMAIN: vol.Schema({
+        vol.Required(CONF_DEVICE_HOST): cv.string,
+        vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_PORT): cv.port,
+        vol.Optional(CONF_DEVICE_PARTITION,
+                     default=DEFAULT_DEVICE_PARTITION): cv.positive_int,
+        vol.Optional(CONF_ARM_HOME_MODE,
+                     default=DEFAULT_CONF_ARM_HOME_MODE): vol.In([1, 2, 3]),
+        vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
+    }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+    """Set up the Satel Integra component."""
+    conf = config.get(DOMAIN)
+
+    zones = conf.get(CONF_ZONES)
+    host = conf.get(CONF_DEVICE_HOST)
+    port = conf.get(CONF_DEVICE_PORT)
+    partition = conf.get(CONF_DEVICE_PARTITION)
+
+    from satel_integra.satel_integra import AsyncSatel, AlarmState
+
+    controller = AsyncSatel(host, port, zones, hass.loop, partition)
+
+    hass.data[DATA_SATEL] = controller
+
+    result = yield from controller.connect()
+
+    if not result:
+        return False
+
+    @asyncio.coroutine
+    def _close():
+        controller.close()
+
+    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close())
+
+    _LOGGER.debug("Arm home config: %s, mode: %s ",
+                  conf,
+                  conf.get(CONF_ARM_HOME_MODE))
+
+    task_control_panel = hass.async_add_job(
+        async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config))
+
+    task_zones = hass.async_add_job(
+        async_load_platform(hass, 'binary_sensor', DOMAIN,
+                            {CONF_ZONES: zones}, config))
+
+    yield from asyncio.wait([task_control_panel, task_zones], loop=hass.loop)
+
+    @callback
+    def alarm_status_update_callback(status):
+        """Send status update received from alarm to home assistant."""
+        _LOGGER.debug("Alarm status callback, status: %s", status)
+        hass_alarm_status = STATE_ALARM_DISARMED
+
+        if status == AlarmState.ARMED_MODE0:
+            hass_alarm_status = STATE_ALARM_ARMED_AWAY
+
+        elif status in [
+                AlarmState.ARMED_MODE0,
+                AlarmState.ARMED_MODE1,
+                AlarmState.ARMED_MODE2,
+                AlarmState.ARMED_MODE3
+        ]:
+            hass_alarm_status = STATE_ALARM_ARMED_HOME
+
+        elif status in [AlarmState.TRIGGERED, AlarmState.TRIGGERED_FIRE]:
+            hass_alarm_status = STATE_ALARM_TRIGGERED
+
+        elif status == AlarmState.DISARMED:
+            hass_alarm_status = STATE_ALARM_DISARMED
+
+        _LOGGER.debug("Sending hass_alarm_status: %s...", hass_alarm_status)
+        async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, hass_alarm_status)
+
+    @callback
+    def zones_update_callback(status):
+        """Update zone objects as per notification from the alarm."""
+        _LOGGER.debug("Zones callback , status: %s", status)
+        async_dispatcher_send(hass, SIGNAL_ZONES_UPDATED, status[ZONES])
+
+    # Create a task instead of adding a tracking job, since this task will
+    # run until the connection to satel_integra is closed.
+    hass.loop.create_task(controller.keep_alive())
+    hass.loop.create_task(
+        controller.monitor_status(
+            alarm_status_update_callback,
+            zones_update_callback)
+    )
+
+    return True
diff --git a/requirements_all.txt b/requirements_all.txt
index 7e88616f673..b58c1f846cf 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -883,6 +883,9 @@ rxv==0.4.0
 # homeassistant.components.media_player.samsungtv
 samsungctl==0.6.0
 
+# homeassistant.components.satel_integra
+satel_integra==0.1.0
+
 # homeassistant.components.sensor.deutsche_bahn
 schiene==0.18
 
-- 
GitLab