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