diff --git a/.coveragerc b/.coveragerc index caadb341b596da01eae0dbdb4fdaea5b191b2fbf..e9856c7a51e940f9123588c19597821341743daa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -108,9 +108,11 @@ omit = homeassistant/components/*/zoneminder.py homeassistant/components/alarm_control_panel/alarmdotcom.py + homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/binary_sensor/arest.py + homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/bloomsky.py diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py new file mode 100755 index 0000000000000000000000000000000000000000..0e0fd026b60dbcdf7f1c40f4513b5b0f20ca3488 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -0,0 +1,136 @@ +""" +Support for Concord232 alarm control panels. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.concord232/ +""" + +import datetime + +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, STATE_UNKNOWN) +import homeassistant.helpers.config_validation as cv + +import requests + +import voluptuous as vol + +REQUIREMENTS = ['concord232==0.14'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'CONCORD232' +DEFAULT_PORT = 5007 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, +}) + +SCAN_INTERVAL = 1 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup concord232 platform.""" + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + + url = 'http://{}:{}'.format(host, port) + + try: + add_devices([Concord232Alarm(hass, url, name)]) + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + return False + + +class Concord232Alarm(alarm.AlarmControlPanel): + """Represents the Concord232-based alarm panel.""" + + def __init__(self, hass, url, name): + """Initalize the concord232 alarm panel.""" + from concord232 import client as concord232_client + + self._state = STATE_UNKNOWN + self._hass = hass + self._name = name + self._url = url + + try: + client = concord232_client.Client(self._url) + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + + self._alarm = client + self._alarm.partitions = self._alarm.list_partitions() + self._alarm.last_partition_update = datetime.datetime.now() + self.update() + + @property + def should_poll(self): + """Polling needed.""" + return True + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def code_format(self): + """The characters if code is defined.""" + return '[0-9]{4}([0-9]{2})?' + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def update(self): + """Update values from API.""" + try: + part = self._alarm.list_partitions()[0] + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to %(host)s: %(reason)s', + dict(host=self._url, reason=ex)) + newstate = STATE_UNKNOWN + except IndexError: + _LOGGER.error('concord232 reports no partitions') + newstate = STATE_UNKNOWN + + if part['arming_level'] == "Off": + newstate = STATE_ALARM_DISARMED + elif "Home" in part['arming_level']: + newstate = STATE_ALARM_ARMED_HOME + else: + newstate = STATE_ALARM_ARMED_AWAY + + if not newstate == self._state: + _LOGGER.info("State Chnage from %s to %s", self._state, newstate) + self._state = newstate + return self._state + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self._alarm.disarm(code) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._alarm.arm('home') + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._alarm.arm('auto') + + def alarm_trigger(self, code=None): + """Alarm trigger command.""" + raise NotImplementedError() diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py new file mode 100755 index 0000000000000000000000000000000000000000..bc1eab4694a104c0119ecb6d01c18e0a412fdc89 --- /dev/null +++ b/homeassistant/components/binary_sensor/concord232.py @@ -0,0 +1,143 @@ +""" +Support for exposing Concord232 elements as sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.concord232/ +""" +import datetime + +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES) +from homeassistant.const import (CONF_HOST, CONF_PORT) + +import homeassistant.helpers.config_validation as cv + +import requests + +import voluptuous as vol + + +REQUIREMENTS = ['concord232==0.14'] + +_LOGGER = logging.getLogger(__name__) + +CONF_EXCLUDE_ZONES = 'exclude_zones' +CONF_ZONE_TYPES = 'zone_types' + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = '5007' +DEFAULT_SSL = False + +ZONE_TYPES_SCHEMA = vol.Schema({ + cv.positive_int: vol.In(SENSOR_CLASSES), +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_EXCLUDE_ZONES, default=[]): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, +}) + +SCAN_INTERVAL = 1 + +DEFAULT_NAME = "Alarm" + + +# pylint: disable=too-many-locals +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Concord232 binary sensor platform.""" + from concord232 import client as concord232_client + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + exclude = config.get(CONF_EXCLUDE_ZONES) + zone_types = config.get(CONF_ZONE_TYPES) + sensors = [] + + try: + _LOGGER.debug('Initializing Client.') + client = concord232_client.Client('http://{}:{}' + .format(host, port)) + client.zones = client.list_zones() + client.last_zone_update = datetime.datetime.now() + + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + return False + + for zone in client.zones: + _LOGGER.info('Loading Zone found: %s', zone['name']) + if zone['number'] not in exclude: + sensors.append(Concord232ZoneSensor( + hass, + client, + zone, + zone_types.get(zone['number'], get_opening_type(zone)))) + + add_devices(sensors) + + return True + + +def get_opening_type(zone): + """Helper function to try to guess sensor type frm name.""" + if "MOTION" in zone["name"]: + return "motion" + if "KEY" in zone["name"]: + return "safety" + if "SMOKE" in zone["name"]: + return "smoke" + if "WATER" in zone["name"]: + return "water" + return "opening" + + +class Concord232ZoneSensor(BinarySensorDevice): + """Representation of a Concord232 zone as a sensor.""" + + def __init__(self, hass, client, zone, zone_type): + """Initialize the Concord232 binary sensor.""" + self._hass = hass + self._client = client + self._zone = zone + self._number = zone['number'] + self._zone_type = zone_type + self.update() + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return self._zone_type + + @property + def should_poll(self): + """No polling needed.""" + return True + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._zone['name'] + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + # True means "faulted" or "open" or "abnormal state" + return bool(self._zone['state'] == 'Normal') + + def update(self): + """"Get updated stats from API.""" + last_update = datetime.datetime.now() - self._client.last_zone_update + _LOGGER.debug("Zone: %s ", self._zone) + if last_update > datetime.timedelta(seconds=1): + self._client.zones = self._client.list_zones() + self._client.last_zone_update = datetime.datetime.now() + _LOGGER.debug("Updated from Zone: %s", self._zone['name']) + + if hasattr(self._client, 'zones'): + self._zone = next((x for x in self._client.zones + if x['number'] == self._number), None) diff --git a/requirements_all.txt b/requirements_all.txt index 240972252ac2ec9a8a4e4e00136b11af482d3dc6..5c44237bb83cee4b9b1f71b8a74a9e57bebd93d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -72,6 +72,10 @@ coinmarketcap==2.0.1 # homeassistant.scripts.check_config colorlog>2.1,<3 +# homeassistant.components.alarm_control_panel.concord232 +# homeassistant.components.binary_sensor.concord232 +concord232==0.14 + # homeassistant.components.media_player.directv directpy==0.1