From f68542ba0d487c3ccf1f1c9447c8c0a995a43565 Mon Sep 17 00:00:00 2001 From: hawk259 Date: Wed, 12 Apr 2017 05:35:35 -0400 Subject: [PATCH] Adding AlarmDecoder platform (#6900) * Added AlarmDecoder platform * remove try/catch for generic execption * Changes for @pvizeli, thanks for the review! Removed _ prefix from normal function variables Removed _hass as it will be set via .hass for us Broke out the three config (socket, serial, usb) and use vol.Any Added support for USB I think, don't have device, but should work Removed components dictionary, was form old group all code that didn't work * Fix hass string handling --- .coveragerc | 3 + .../alarm_control_panel/alarmdecoder.py | 119 ++++++++++++ homeassistant/components/alarmdecoder.py | 171 ++++++++++++++++++ .../components/binary_sensor/alarmdecoder.py | 123 +++++++++++++ .../components/sensor/alarmdecoder.py | 75 ++++++++ requirements_all.txt | 3 + 6 files changed, 494 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/alarmdecoder.py create mode 100644 homeassistant/components/alarmdecoder.py create mode 100644 homeassistant/components/binary_sensor/alarmdecoder.py create mode 100644 homeassistant/components/sensor/alarmdecoder.py diff --git a/.coveragerc b/.coveragerc index 578276cac43..b2f6a21c84b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,9 @@ omit = homeassistant/helpers/signal.py # omit pieces of code that rely on external devices being present + homeassistant/components/alarmdecoder.py + homeassistant/components/*/alarmdecoder.py + homeassistant/components/apcupsd.py homeassistant/components/*/apcupsd.py diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py new file mode 100644 index 00000000000..f176a87827b --- /dev/null +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -0,0 +1,119 @@ +""" +Support for AlarmDecoder-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.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.components.alarm_control_panel as alarm + +from homeassistant.components.alarmdecoder import (DATA_AD, + SIGNAL_PANEL_MESSAGE) + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['alarmdecoder'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Perform the setup for AlarmDecoder alarm panels.""" + _LOGGER.debug("AlarmDecoderAlarmPanel: setup") + + device = AlarmDecoderAlarmPanel("Alarm Panel", hass) + + async_add_devices([device]) + + return True + + +class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): + """Representation of an AlarmDecoder-based alarm panel.""" + + def __init__(self, name, hass): + """Initialize the alarm panel.""" + self._display = "" + self._name = name + self._state = STATE_UNKNOWN + + _LOGGER.debug("AlarmDecoderAlarm: Setting up panel") + + @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.alarm_sounding or message.fire_alarm: + if self._state != STATE_ALARM_TRIGGERED: + self._state = STATE_ALARM_TRIGGERED + self.hass.async_add_job(self.async_update_ha_state()) + elif message.armed_away: + if self._state != STATE_ALARM_ARMED_AWAY: + self._state = STATE_ALARM_ARMED_AWAY + self.hass.async_add_job(self.async_update_ha_state()) + elif message.armed_home: + if self._state != STATE_ALARM_ARMED_HOME: + self._state = STATE_ALARM_ARMED_HOME + self.hass.async_add_job(self.async_update_ha_state()) + else: + if self._state != STATE_ALARM_DISARMED: + self._state = STATE_ALARM_DISARMED + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def code_format(self): + """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.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_disarm: sending %s1", + str(code)) + self.hass.data[DATA_AD].send("{!s}1".format(code)) + + @asyncio.coroutine + def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_away: sending %s2", + str(code)) + self.hass.data[DATA_AD].send("{!s}2".format(code)) + + @asyncio.coroutine + def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: %s", code) + if code: + _LOGGER.debug("AlarmDecoderAlarm::alarm_arm_home: sending %s3", + str(code)) + self.hass.data[DATA_AD].send("{!s}3".format(code)) diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py new file mode 100644 index 00000000000..ec99f2381e5 --- /dev/null +++ b/homeassistant/components/alarmdecoder.py @@ -0,0 +1,171 @@ +""" +Support for AlarmDecoder devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/alarmdecoder/ +""" +import asyncio +import logging + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['alarmdecoder==0.12.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'alarmdecoder' + +DATA_AD = 'alarmdecoder' + + +CONF_DEVICE = 'device' +CONF_DEVICE_TYPE = 'type' +CONF_DEVICE_HOST = 'host' +CONF_DEVICE_PORT = 'port' +CONF_DEVICE_PATH = 'path' +CONF_DEVICE_BAUD = 'baudrate' + +CONF_ZONES = 'zones' +CONF_ZONE_NAME = 'name' +CONF_ZONE_TYPE = 'type' + +CONF_PANEL_DISPLAY = 'panel_display' + +DEFAULT_DEVICE_TYPE = 'socket' +DEFAULT_DEVICE_HOST = 'localhost' +DEFAULT_DEVICE_PORT = 10000 +DEFAULT_DEVICE_PATH = '/dev/ttyUSB0' +DEFAULT_DEVICE_BAUD = 115200 + +DEFAULT_PANEL_DISPLAY = False + +DEFAULT_ZONE_TYPE = 'opening' + +SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message' +SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away' +SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home' +SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm' + +SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' +SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' + +DEVICE_SOCKET_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'socket', + vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string, + vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port}) + +DEVICE_SERIAL_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'serial', + vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string, + vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string}) + +DEVICE_USB_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICE_TYPE): 'usb'}) + +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): vol.Any(DEVICE_SOCKET_SCHEMA, + DEVICE_SERIAL_SCHEMA, + DEVICE_USB_SCHEMA), + vol.Optional(CONF_PANEL_DISPLAY, + default=DEFAULT_PANEL_DISPLAY): cv.boolean, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + }), +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Common setup for AlarmDecoder devices.""" + from alarmdecoder import AlarmDecoder + from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice) + + conf = config.get(DOMAIN) + + device = conf.get(CONF_DEVICE) + display = conf.get(CONF_PANEL_DISPLAY) + zones = conf.get(CONF_ZONES) + + device_type = device.get(CONF_DEVICE_TYPE) + host = DEFAULT_DEVICE_HOST + port = DEFAULT_DEVICE_PORT + path = DEFAULT_DEVICE_PATH + baud = DEFAULT_DEVICE_BAUD + + sync_connect = asyncio.Future(loop=hass.loop) + + def handle_open(device): + """Callback for a successful connection.""" + _LOGGER.info("Established a connection with the alarmdecoder.") + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) + sync_connect.set_result(True) + + @callback + def stop_alarmdecoder(event): + """Callback to handle shutdown alarmdecoder.""" + _LOGGER.debug("Shutting down alarmdecoder.") + controller.close() + + @callback + def handle_message(sender, message): + """Callback to handle message from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message) + + def zone_fault_callback(sender, zone): + """Callback to handle zone fault from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone) + + def zone_restore_callback(sender, zone): + """Callback to handle zone restore from alarmdecoder.""" + async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone) + + controller = False + if device_type == 'socket': + host = device.get(CONF_DEVICE_HOST) + port = device.get(CONF_DEVICE_PORT) + controller = AlarmDecoder(SocketDevice(interface=(host, port))) + elif device_type == 'serial': + path = device.get(CONF_DEVICE_PATH) + baud = device.get(CONF_DEVICE_BAUD) + controller = AlarmDecoder(SerialDevice(interface=path)) + elif device_type == 'usb': + AlarmDecoder(USBDevice.find()) + return False + + controller.on_open += handle_open + controller.on_message += handle_message + controller.on_zone_fault += zone_fault_callback + controller.on_zone_restore += zone_restore_callback + + hass.data[DATA_AD] = controller + + controller.open(baud) + + result = yield from sync_connect + + if not result: + return False + + hass.async_add_job(async_load_platform(hass, 'alarm_control_panel', DOMAIN, + conf, config)) + + if zones: + hass.async_add_job(async_load_platform( + hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) + + if display: + hass.async_add_job(async_load_platform(hass, 'sensor', DOMAIN, + conf, config)) + + return True diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py new file mode 100644 index 00000000000..e6292128710 --- /dev/null +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -0,0 +1,123 @@ +""" +Support for AlarmDecoder 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.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import BinarySensorDevice + +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_OPEN, STATE_CLOSED) +from homeassistant.components.alarmdecoder import (ZONE_SCHEMA, + CONF_ZONES, + CONF_ZONE_NAME, + CONF_ZONE_TYPE, + SIGNAL_ZONE_FAULT, + SIGNAL_ZONE_RESTORE) + + +DEPENDENCIES = ['alarmdecoder'] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup AlarmDecoder binary sensor devices.""" + configured_zones = discovery_info[CONF_ZONES] + + devices = [] + + for zone_num in configured_zones: + device_config_data = ZONE_SCHEMA(configured_zones[zone_num]) + zone_type = device_config_data[CONF_ZONE_TYPE] + zone_name = device_config_data[CONF_ZONE_NAME] + device = AlarmDecoderBinarySensor(hass, + zone_num, + zone_name, + zone_type) + devices.append(device) + + async_add_devices(devices) + + return True + + +class AlarmDecoderBinarySensor(BinarySensorDevice): + """Representation of an AlarmDecoder binary sensor.""" + + def __init__(self, hass, zone_number, zone_name, zone_type): + """Initialize the binary_sensor.""" + self._zone_number = zone_number + self._zone_type = zone_type + self._state = 0 + self._name = zone_name + self._type = zone_type + + _LOGGER.debug('AlarmDecoderBinarySensor: Setup up zone: ' + zone_name) + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) + + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback) + + @property + def state(self): + """Return the state of the binary sensor.""" + if self._type == 'opening': + return STATE_OPEN if self.is_on else STATE_CLOSED + + return STATE_ON if self.is_on else STATE_OFF + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def icon(self): + """Icon for device by its type.""" + if "window" in self._name.lower(): + return "mdi:window-open" if self.is_on else "mdi:window-closed" + + if self._type == 'smoke': + return "mdi:fire" + + return None + + @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 _fault_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 1 + self.hass.async_add_job(self.async_update_ha_state()) + + @callback + def _restore_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self._state = 0 + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py new file mode 100644 index 00000000000..88246cc0bc2 --- /dev/null +++ b/homeassistant/components/sensor/alarmdecoder.py @@ -0,0 +1,75 @@ +""" +Support for AlarmDecoder Sensors (Shows Panel Display). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.alarmdecoder/ +""" +import asyncio +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE) + +from homeassistant.const import (STATE_UNKNOWN) + +DEPENDENCIES = ['alarmdecoder'] + +_LOGGER = logging.getLogger(__name__) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Perform the setup for AlarmDecoder sensor devices.""" + _LOGGER.debug("AlarmDecoderSensor: async_setup_platform") + + device = AlarmDecoderSensor(hass) + + async_add_devices([device]) + + +class AlarmDecoderSensor(Entity): + """Representation of an AlarmDecoder keypad.""" + + def __init__(self, hass): + """Initialize the alarm panel.""" + self._display = "" + self._state = STATE_UNKNOWN + self._icon = 'mdi:alarm-check' + self._name = 'Alarm Panel Display' + + _LOGGER.debug("AlarmDecoderSensor: Setting up panel") + + @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 self._display != message.text: + self._display = message.text + self.hass.async_add_job(self.async_update_ha_state()) + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._display + + @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/requirements_all.txt b/requirements_all.txt index a37ecb1f775..cbecf913943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,6 +47,9 @@ aiohttp_cors==0.5.2 # homeassistant.components.light.lifx aiolifx==0.4.4 +# homeassistant.components.alarmdecoder +alarmdecoder==0.12.1.0 + # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest amcrest==1.1.8