From b45dad507a0275c91f5142c10ce420545324e965 Mon Sep 17 00:00:00 2001
From: Mattias Welponer <mattias@welponer.net>
Date: Sun, 18 Mar 2018 16:57:53 +0100
Subject: [PATCH] Add initial support fo HomematicIP components (#12761)

* Add initial support fo HomematicIP components

* Fix module import

* Update reqirments file as well

* Added HomematicIP files

* Update to homematicip

* Code cleanup based on highligted issues

* Update of reqiremnets file as well

* Fix dispatcher usage

* Rename homematicip to homematicip_cloud
---
 .coveragerc                                   |   3 +
 homeassistant/components/homematicip_cloud.py | 170 ++++++++++++
 .../components/sensor/homematicip_cloud.py    | 258 ++++++++++++++++++
 requirements_all.txt                          |   3 +
 4 files changed, 434 insertions(+)
 create mode 100644 homeassistant/components/homematicip_cloud.py
 create mode 100644 homeassistant/components/sensor/homematicip_cloud.py

diff --git a/.coveragerc b/.coveragerc
index 4da5343bf4f..d98048636c3 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -109,6 +109,9 @@ omit =
     homeassistant/components/homematic/__init__.py
     homeassistant/components/*/homematic.py
 
+    homeassistant/components/homematicip_cloud.py
+    homeassistant/components/*/homematicip_cloud.py
+
     homeassistant/components/ihc/*
     homeassistant/components/*/ihc.py
 
diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py
new file mode 100644
index 00000000000..a89678624eb
--- /dev/null
+++ b/homeassistant/components/homematicip_cloud.py
@@ -0,0 +1,170 @@
+"""
+Support for HomematicIP components.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/homematicip/
+"""
+
+import logging
+from socket import timeout
+
+import voluptuous as vol
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import (dispatcher_send,
+                                              async_dispatcher_connect)
+from homeassistant.helpers.discovery import load_platform
+from homeassistant.helpers.entity import Entity
+
+REQUIREMENTS = ['homematicip==0.8']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'homematicip_cloud'
+
+CONF_NAME = 'name'
+CONF_ACCESSPOINT = 'accesspoint'
+CONF_AUTHTOKEN = 'authtoken'
+
+CONFIG_SCHEMA = vol.Schema({
+    vol.Optional(DOMAIN): [vol.Schema({
+        vol.Optional(CONF_NAME, default=''): cv.string,
+        vol.Required(CONF_ACCESSPOINT): cv.string,
+        vol.Required(CONF_AUTHTOKEN): cv.string,
+    })],
+}, extra=vol.ALLOW_EXTRA)
+
+EVENT_HOME_CHANGED = 'homematicip_home_changed'
+EVENT_DEVICE_CHANGED = 'homematicip_device_changed'
+EVENT_GROUP_CHANGED = 'homematicip_group_changed'
+EVENT_SECURITY_CHANGED = 'homematicip_security_changed'
+EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed'
+
+ATTR_HOME_ID = 'home_id'
+ATTR_HOME_LABEL = 'home_label'
+ATTR_DEVICE_ID = 'device_id'
+ATTR_DEVICE_LABEL = 'device_label'
+ATTR_STATUS_UPDATE = 'status_update'
+ATTR_FIRMWARE_STATE = 'firmware_state'
+ATTR_LOW_BATTERY = 'low_battery'
+ATTR_SABOTAGE = 'sabotage'
+ATTR_RSSI = 'rssi'
+
+
+def setup(hass, config):
+    """Set up the HomematicIP component."""
+    # pylint: disable=import-error, no-name-in-module
+    from homematicip.home import Home
+    hass.data.setdefault(DOMAIN, {})
+    homes = hass.data[DOMAIN]
+    accesspoints = config.get(DOMAIN, [])
+
+    def _update_event(events):
+        """Handle incoming HomeMaticIP events."""
+        for event in events:
+            etype = event['eventType']
+            edata = event['data']
+            if etype == 'DEVICE_CHANGED':
+                dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id)
+            elif etype == 'GROUP_CHANGED':
+                dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id)
+            elif etype == 'HOME_CHANGED':
+                dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id)
+            elif etype == 'JOURNAL_CHANGED':
+                dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id)
+        return True
+
+    for device in accesspoints:
+        name = device.get(CONF_NAME)
+        accesspoint = device.get(CONF_ACCESSPOINT)
+        authtoken = device.get(CONF_AUTHTOKEN)
+
+        home = Home()
+        if name.lower() == 'none':
+            name = ''
+        home.label = name
+        try:
+            home.set_auth_token(authtoken)
+            home.init(accesspoint)
+            if home.get_current_state():
+                _LOGGER.info("Connection to HMIP established")
+            else:
+                _LOGGER.warning("Connection to HMIP could not be established")
+                return False
+        except timeout:
+            _LOGGER.warning("Connection to HMIP could not be established")
+            return False
+        homes[home.id] = home
+        home.onEvent += _update_event
+        home.enable_events()
+        _LOGGER.info('HUB name: %s, id: %s', home.label, home.id)
+
+        for component in ['sensor']:
+            load_platform(hass, component, DOMAIN,
+                          {'homeid': home.id}, config)
+    return True
+
+
+class HomematicipGenericDevice(Entity):
+    """Representation of an HomematicIP generic device."""
+
+    def __init__(self, hass, home, device, signal=None):
+        """Initialize the generic device."""
+        self.hass = hass
+        self._home = home
+        self._device = device
+        async_dispatcher_connect(
+            self.hass, EVENT_DEVICE_CHANGED, self._device_changed)
+
+    @callback
+    def _device_changed(self, deviceid):
+        """Handle device state changes."""
+        if deviceid is None or deviceid == self._device.id:
+            _LOGGER.debug('Event device %s', self._device.label)
+            self.async_schedule_update_ha_state()
+
+    def _name(self, addon=''):
+        """Return the name of the device."""
+        name = ''
+        if self._home.label != '':
+            name += self._home.label + ' '
+        name += self._device.label
+        if addon != '':
+            name += ' ' + addon
+        return name
+
+    @property
+    def name(self):
+        """Return the name of the generic device."""
+        return self._name()
+
+    @property
+    def should_poll(self):
+        """No polling needed."""
+        return False
+
+    @property
+    def available(self):
+        """Device available."""
+        return not self._device.unreach
+
+    def _generic_state_attributes(self):
+        """Return the state attributes of the generic device."""
+        laststatus = ''
+        if self._device.lastStatusUpdate is not None:
+            laststatus = self._device.lastStatusUpdate.isoformat()
+        return {
+            ATTR_HOME_LABEL: self._home.label,
+            ATTR_DEVICE_LABEL: self._device.label,
+            ATTR_HOME_ID: self._device.homeId,
+            ATTR_DEVICE_ID: self._device.id.lower(),
+            ATTR_STATUS_UPDATE: laststatus,
+            ATTR_FIRMWARE_STATE: self._device.updateState.lower(),
+            ATTR_LOW_BATTERY: self._device.lowBat,
+            ATTR_RSSI: self._device.rssiDeviceValue,
+        }
+
+    @property
+    def device_state_attributes(self):
+        """Return the state attributes of the generic device."""
+        return self._generic_state_attributes()
diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py
new file mode 100644
index 00000000000..8f298bbb3f6
--- /dev/null
+++ b/homeassistant/components/sensor/homematicip_cloud.py
@@ -0,0 +1,258 @@
+"""
+Support for HomematicIP sensors.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/homematicip/
+"""
+
+import logging
+
+from homeassistant.core import callback
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.dispatcher import dispatcher_connect
+from homeassistant.components.homematicip_cloud import (
+    HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED,
+    ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI)
+from homeassistant.const import TEMP_CELSIUS, STATE_OK
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['homematicip_cloud']
+
+ATTR_VALVE_STATE = 'valve_state'
+ATTR_VALVE_POSITION = 'valve_position'
+ATTR_TEMPERATURE_OFFSET = 'temperature_offset'
+
+HMIP_UPTODATE = 'up_to_date'
+HMIP_VALVE_DONE = 'adaption_done'
+HMIP_SABOTAGE = 'sabotage'
+
+STATE_LOW_BATTERY = 'low_battery'
+STATE_SABOTAGE = 'sabotage'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the HomematicIP sensors devices."""
+    # pylint: disable=import-error, no-name-in-module
+    from homematicip.device import (
+        HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
+        TemperatureHumiditySensorDisplay)
+
+    _LOGGER.info('Setting up HomeMaticIP accespoint & generic devices')
+    homeid = discovery_info['homeid']
+    home = hass.data[DOMAIN][homeid]
+    devices = [HomematicipAccesspoint(hass, home)]
+    if home.devices is None:
+        return
+    for device in home.devices:
+        devices.append(HomematicipDeviceStatus(hass, home, device))
+        if isinstance(device, HeatingThermostat):
+            devices.append(HomematicipHeatingThermostat(hass, home, device))
+        if isinstance(device, TemperatureHumiditySensorWithoutDisplay):
+            devices.append(HomematicipSensorThermometer(hass, home, device))
+            devices.append(HomematicipSensorHumidity(hass, home, device))
+        if isinstance(device, TemperatureHumiditySensorDisplay):
+            devices.append(HomematicipSensorThermometer(hass, home, device))
+            devices.append(HomematicipSensorHumidity(hass, home, device))
+    add_devices(devices)
+
+
+class HomematicipAccesspoint(Entity):
+    """Representation of an HomeMaticIP access point."""
+
+    def __init__(self, hass, home):
+        """Initialize the access point sensor."""
+        self.hass = hass
+        self._home = home
+        dispatcher_connect(
+            self.hass, EVENT_HOME_CHANGED, self._home_changed)
+        _LOGGER.debug('Setting up access point %s', home.label)
+
+    @callback
+    def _home_changed(self, deviceid):
+        """Handle device state changes."""
+        if deviceid is None or deviceid == self._home.id:
+            _LOGGER.debug('Event access point %s', self._home.label)
+            self.async_schedule_update_ha_state()
+
+    @property
+    def name(self):
+        """Return the name of the access point device."""
+        if self._home.label == '':
+            return 'Access Point Status'
+        return '{} Access Point Status'.format(self._home.label)
+
+    @property
+    def icon(self):
+        """Return the icon of the access point device."""
+        return 'mdi:access-point-network'
+
+    @property
+    def state(self):
+        """Return the state of the access point."""
+        return self._home.dutyCycle
+
+    @property
+    def available(self):
+        """Device available."""
+        return self._home.connected
+
+    @property
+    def device_state_attributes(self):
+        """Return the state attributes of the access point."""
+        return {
+            ATTR_HOME_LABEL: self._home.label,
+            ATTR_HOME_ID: self._home.id,
+            }
+
+
+class HomematicipDeviceStatus(HomematicipGenericDevice):
+    """Representation of an HomematicIP device status."""
+
+    def __init__(self, hass, home, device, signal=None):
+        """Initialize the device."""
+        super().__init__(hass, home, device)
+        _LOGGER.debug('Setting up sensor device status: %s', device.label)
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._name('Status')
+
+    @property
+    def icon(self):
+        """Return the icon of the status device."""
+        if (hasattr(self._device, 'sabotage') and
+                self._device.sabotage == HMIP_SABOTAGE):
+            return 'mdi:alert'
+        elif self._device.lowBat:
+            return 'mdi:battery-outline'
+        elif self._device.updateState.lower() != HMIP_UPTODATE:
+            return 'mdi:refresh'
+        return 'mdi:check'
+
+    @property
+    def state(self):
+        """Return the state of the generic device."""
+        if (hasattr(self._device, 'sabotage') and
+                self._device.sabotage == HMIP_SABOTAGE):
+            return STATE_SABOTAGE
+        elif self._device.lowBat:
+            return STATE_LOW_BATTERY
+        elif self._device.updateState.lower() != HMIP_UPTODATE:
+            return self._device.updateState.lower()
+        return STATE_OK
+
+
+class HomematicipHeatingThermostat(HomematicipGenericDevice):
+    """MomematicIP heating thermostat representation."""
+
+    def __init__(self, hass, home, device):
+        """"Initialize heating thermostat."""
+        super().__init__(hass, home, device)
+        _LOGGER.debug('Setting up heating thermostat device: %s', device.label)
+
+    @property
+    def icon(self):
+        """Return the icon."""
+        if self._device.valveState.lower() != HMIP_VALVE_DONE:
+            return 'mdi:alert'
+        return 'mdi:radiator'
+
+    @property
+    def state(self):
+        """Return the state of the radiator valve."""
+        if self._device.valveState.lower() != HMIP_VALVE_DONE:
+            return self._device.valveState.lower()
+        return round(self._device.valvePosition*100)
+
+    @property
+    def unit_of_measurement(self):
+        """Return the unit this state is expressed in."""
+        return '%'
+
+    @property
+    def device_state_attributes(self):
+        """Return the state attributes."""
+        return {
+            ATTR_VALVE_STATE: self._device.valveState.lower(),
+            ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset,
+            ATTR_LOW_BATTERY: self._device.lowBat,
+            ATTR_RSSI: self._device.rssiDeviceValue
+        }
+
+
+class HomematicipSensorHumidity(HomematicipGenericDevice):
+    """MomematicIP thermometer device."""
+
+    def __init__(self, hass, home, device):
+        """"Initialize the thermometer device."""
+        super().__init__(hass, home, device)
+        _LOGGER.debug('Setting up humidity device: %s',
+                      device.label)
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._name('Humidity')
+
+    @property
+    def icon(self):
+        """Return the icon."""
+        return 'mdi:water'
+
+    @property
+    def state(self):
+        """Return the state."""
+        return self._device.humidity
+
+    @property
+    def unit_of_measurement(self):
+        """Return the unit this state is expressed in."""
+        return '%'
+
+    @property
+    def device_state_attributes(self):
+        """Return the state attributes."""
+        return {
+            ATTR_LOW_BATTERY: self._device.lowBat,
+            ATTR_RSSI: self._device.rssiDeviceValue,
+        }
+
+
+class HomematicipSensorThermometer(HomematicipGenericDevice):
+    """MomematicIP thermometer device."""
+
+    def __init__(self, hass, home, device):
+        """"Initialize the thermometer device."""
+        super().__init__(hass, home, device)
+        _LOGGER.debug('Setting up thermometer device: %s', device.label)
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._name('Temperature')
+
+    @property
+    def icon(self):
+        """Return the icon."""
+        return 'mdi:thermometer'
+
+    @property
+    def state(self):
+        """Return the state."""
+        return self._device.actualTemperature
+
+    @property
+    def unit_of_measurement(self):
+        """Return the unit this state is expressed in."""
+        return TEMP_CELSIUS
+
+    @property
+    def device_state_attributes(self):
+        """Return the state attributes."""
+        return {
+            ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset,
+            ATTR_LOW_BATTERY: self._device.lowBat,
+            ATTR_RSSI: self._device.rssiDeviceValue,
+        }
diff --git a/requirements_all.txt b/requirements_all.txt
index 2da1c3a6990..fbddbe9c448 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -358,6 +358,9 @@ holidays==0.9.4
 # homeassistant.components.frontend
 home-assistant-frontend==20180316.0
 
+# homeassistant.components.homematicip_cloud
+homematicip==0.8
+
 # homeassistant.components.camera.onvif
 http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
 
-- 
GitLab