diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 180d6943d8a40c..0ed9fe22e275a1 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -5,143 +5,181 @@ https://home-assistant.io/components/homematicip_cloud/ """ +import asyncio import logging -from socket import timeout - import voluptuous as vol -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.discovery import async_load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homematicip==0.8'] +REQUIREMENTS = ['homematicip==0.9.2.4'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' +COMPONENTS = [ + 'sensor' +] + 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.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME): vol.Any(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' +HMIP_ACCESS_POINT = 'Access Point' +HMIP_HUB = 'HmIP-HUB' ATTR_HOME_ID = 'home_id' -ATTR_HOME_LABEL = 'home_label' +ATTR_HOME_NAME = 'home_name' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_LABEL = 'device_label' ATTR_STATUS_UPDATE = 'status_update' ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_UNREACHABLE = 'unreachable' ATTR_LOW_BATTERY = 'low_battery' +ATTR_MODEL_TYPE = 'model_type' +ATTR_GROUP_TYPE = 'group_type' +ATTR_DEVICE_RSSI = 'device_rssi' +ATTR_DUTY_CYCLE = 'duty_cycle' +ATTR_CONNECTED = 'connected' ATTR_SABOTAGE = 'sabotage' -ATTR_RSSI = 'rssi' -ATTR_TYPE = 'type' +ATTR_OPERATION_LOCK = 'operation_lock' -def setup(hass, config): +async def async_setup(hass, config): """Set up the HomematicIP component.""" - # pylint: disable=import-error, no-name-in-module - from homematicip.home import Home + from homematicip.base.base_connection import HmipConnectionError 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 + for conf in accesspoints: + _websession = async_get_clientsession(hass) + _hmip = HomematicipConnector(hass, conf, _websession) 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") + await _hmip.init() + except HmipConnectionError: + _LOGGER.error('Failed to connect to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) 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) + home = _hmip.home + home.name = conf.get(CONF_NAME) + home.label = HMIP_ACCESS_POINT + home.modelType = HMIP_HUB + hass.data[DOMAIN][home.id] = home + _LOGGER.info('Connected to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) + homeid = {ATTR_HOME_ID: home.id} + for component in COMPONENTS: + hass.async_add_job(async_load_platform(hass, component, DOMAIN, + homeid, config)) + + hass.loop.create_task(_hmip.connect()) return True +class HomematicipConnector: + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config, websession): + """Initialize HomematicIP cloud connection.""" + from homematicip.async.home import AsyncHome + self._hass = hass + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint = config.get(CONF_ACCESSPOINT) + _authtoken = config.get(CONF_AUTHTOKEN) + + self.home = AsyncHome(hass.loop, websession) + self.home.set_auth_token(_authtoken) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) + + async def init(self): + """Initialize connection.""" + await self.home.init(self._accesspoint) + await self.home.get_current_state() + + async def _handle_connection(self): + """Handle websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def connect(self): + """Start websocket connection.""" + self._tries = 0 + while True: + await self._handle_connection() + if self._ws_close_requested: + break + self._ws_close_requested = False + self._tries += 1 + try: + self._retry_task = self._hass.async_add_job(asyncio.sleep( + 2 ** min(9, self._tries), loop=self._hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + _LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.', + self._tries) + + async def close(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_task is not None: + self._retry_task.cancel() + await self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home, device): + def __init__(self, home, device, post=None): """Initialize the generic device.""" self._home = home self._device = device + self.post = post + _LOGGER.info('Setting up %s (%s)', self.name, + self._device.modelType) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + self._device.on_update(self._device_changed) - @callback - def _device_changed(self, deviceid): + def _device_changed(self, json, **kwargs): """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 + _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) + self.async_schedule_update_ha_state() @property def name(self): """Return the name of the generic device.""" - return self._name() + name = self._device.label + if self._home.name is not None: + name = "{} {}".format(self._home.name, name) + if self.post is not None: + name = "{} {}".format(name, self.post) + return name @property def should_poll(self): @@ -153,24 +191,10 @@ def available(self): """Device available.""" return not self._device.unreach - def _generic_state_attributes(self): + @property + def device_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, - ATTR_TYPE: self._device.modelType + ATTR_MODEL_TYPE: self._device.modelType } - - @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 index 1a37aa1ad4e071..aa350f7be5d048 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -7,13 +7,10 @@ import logging -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_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 + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) +from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) @@ -21,68 +18,49 @@ ATTR_VALVE_STATE = 'valve_state' ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE_OFFSET = 'temperature_offset' +ATTR_HUMIDITY = 'humidity' HMIP_UPTODATE = 'up_to_date' HMIP_VALVE_DONE = 'adaption_done' HMIP_SABOTAGE = 'sabotage' +STATE_OK = 'ok' STATE_LOW_BATTERY = 'low_battery' STATE_SABOTAGE = 'sabotage' -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_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) - homeid = discovery_info['homeid'] - home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(home)] + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [HomematicipAccesspointStatus(home)] for device in home.devices: - devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) - if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) - if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) + if isinstance(device, (TemperatureHumiditySensorDisplay, + TemperatureHumiditySensorWithoutDisplay)): + devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHumiditySensor(home, device)) - if home.devices: - add_devices(devices) + if devices: + async_add_devices(devices) -class HomematicipAccesspoint(Entity): +class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP access point.""" def __init__(self, home): - """Initialize the access point sensor.""" - self._home = home - _LOGGER.debug('Setting up access point %s', home.label) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) - - @callback - def _home_changed(self, deviceid): - """Handle device state changes.""" - if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event home %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) + """Initialize access point device.""" + super().__init__(home, home) @property def icon(self): @@ -102,24 +80,15 @@ def available(self): @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, - } + return {} class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" def __init__(self, home, device): - """Initialize the device.""" - super().__init__(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') + """Initialize generic status device.""" + super().__init__(home, device, 'Status') @property def icon(self): @@ -150,9 +119,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" def __init__(self, home, device): - """"Initialize heating thermostat.""" - super().__init__(home, device) - _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + """Initialize heating thermostat device.""" + super().__init__(home, device, 'Heating') @property def icon(self): @@ -173,34 +141,18 @@ 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.""" +class HomematicipHumiditySensor(HomematicipGenericDevice): + """MomematicIP humidity device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(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') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Humidity') @property def icon(self): """Return the icon.""" - return 'mdi:water' + return 'mdi:water-percent' @property def state(self): @@ -212,27 +164,13 @@ 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.""" +class HomematicipTemperatureSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(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') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Temperature') @property def icon(self): @@ -248,12 +186,3 @@ def state(self): 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 e51bbc98823c64..d078b7730ea4b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ holidays==0.9.4 home-assistant-frontend==20180322.0 # homeassistant.components.homematicip_cloud -homematicip==0.8 +homematicip==0.9.2.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a