diff --git a/.gitignore b/.gitignore index fbfa7d1..07e7a7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/*.pyc +.idea/ diff --git a/README.md b/README.md index 040e390..af8ef35 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ source bin/activate pip3 install requests_oauthlib ``` -* Following the [instructions on the Miele developer site](https://www.miele.com/developer/getinvolved.html), you need to request your personal ```ClientID``` and ```ClientSecret```. +* Following the [instructions on the Miele developer site](https://www.miele.com/f/com/en/register_api.aspx), you need to request your personal ```ClientID``` and ```ClientSecret```. ## Installation of the custom component diff --git a/miele/__init__.py b/custom_components/miele/__init__.py similarity index 94% rename from miele/__init__.py rename to custom_components/miele/__init__.py index 72a790c..acda387 100644 --- a/miele/__init__.py +++ b/custom_components/miele/__init__.py @@ -17,13 +17,10 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_time_interval import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.network import get_url from .miele_at_home import MieleClient, MieleOAuth -REQUIREMENTS = ['requests_oauthlib'] - -DEPENDENCIES = ['http'] - _LOGGER = logging.getLogger(__name__) DEVICES = [] @@ -50,7 +47,7 @@ CONFIGURATOR_SUBMIT_CAPTION = 'I have authorized Miele@home.' CONFIGURATOR_DESCRIPTION = 'To link your Miele account, ' \ 'click the link, login, and authorize:' -CONFIGURATOR_DESCRIPTION_IMAGE='https://api.mcs3.miele.com/images/miele-logo-immer-besser.svg' +CONFIGURATOR_DESCRIPTION_IMAGE='https://api.mcs3.miele.com/assets/images/miele_logo.svg' MIELE_COMPONENTS = ['binary_sensor', 'light', 'sensor'] @@ -80,7 +77,7 @@ async def miele_configuration_callback(callback_data): configurator = hass.components.configurator _CONFIGURING[DOMAIN] = configurator.async_request_config( - DEFAULT_NAME, + DEFAULT_NAME, miele_configuration_callback, link_name=CONFIGURATOR_LINK_NAME, link_url=oauth.authorization_url, @@ -108,7 +105,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} if DATA_OAUTH not in hass.data[DOMAIN]: - callback_url = '{}{}'.format(hass.config.api.base_url, AUTH_CALLBACK_PATH) + callback_url = '{}{}'.format(get_url(hass), AUTH_CALLBACK_PATH) cache = config[DOMAIN].get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) hass.data[DOMAIN][DATA_OAUTH] = MieleOAuth( config[DOMAIN].get(CONF_CLIENT_ID), config[DOMAIN].get(CONF_CLIENT_SECRET), @@ -125,19 +122,20 @@ async def async_setup(hass, config): component = EntityComponent(_LOGGER, DOMAIN, hass) - client = MieleClient(hass.data[DOMAIN][DATA_OAUTH]) + client = MieleClient(hass, hass.data[DOMAIN][DATA_OAUTH]) hass.data[DOMAIN][DATA_CLIENT] = client - hass.data[DOMAIN][DATA_DEVICES] = _to_dict(client.get_devices(lang)) + data_get_devices = await client.get_devices(lang) + hass.data[DOMAIN][DATA_DEVICES] = _to_dict(data_get_devices) DEVICES.extend([create_sensor(client, hass, home_device, lang) for k, home_device in hass.data[DOMAIN][DATA_DEVICES].items()]) await component.async_add_entities(DEVICES, False) - + for component in MIELE_COMPONENTS: load_platform(hass, component, DOMAIN, {}, config) - def refresh_devices(event_time): + async def refresh_devices(event_time): _LOGGER.debug("Attempting to update Miele devices") - device_state = client.get_devices(lang) + device_state = await client.get_devices(lang) if device_state is None: _LOGGER.error("Did not receive Miele devices") else: @@ -290,6 +288,6 @@ async def action(self, action): async def async_update(self): if not self.unique_id in self._hass.data[DOMAIN][DATA_DEVICES]: - _LOGGER.error('Miele device not found: {}'.format(self.unique_id)) + _LOGGER.debug('Miele device not found: {}'.format(self.unique_id)) else: self._home_device = self._hass.data[DOMAIN][DATA_DEVICES][self.unique_id] diff --git a/miele/binary_sensor.py b/custom_components/miele/binary_sensor.py similarity index 91% rename from miele/binary_sensor.py rename to custom_components/miele/binary_sensor.py index 1665c45..08a74d4 100644 --- a/miele/binary_sensor.py +++ b/custom_components/miele/binary_sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta from homeassistant.helpers.entity import Entity -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorEntity from custom_components.miele import DOMAIN as MIELE_DOMAIN, DATA_DEVICES @@ -13,6 +13,7 @@ ALL_DEVICES = [] + def _map_key(key): if key == 'signalInfo': return 'Info' @@ -21,9 +22,9 @@ def _map_key(key): elif key == 'signalDoor': return 'Door' + # pylint: disable=W0612 def setup_platform(hass, config, add_devices, discovery_info=None): - global ALL_DEVICES devices = hass.data[MIELE_DOMAIN][DATA_DEVICES] @@ -41,11 +42,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(binary_devices) ALL_DEVICES = ALL_DEVICES + binary_devices + def update_device_state(): for device in ALL_DEVICES: device.async_schedule_update_ha_state(True) -class MieleBinarySensor(BinarySensorDevice): + +class MieleBinarySensor(BinarySensorEntity): def __init__(self, hass, device, key): self._hass = hass @@ -67,7 +70,7 @@ def unique_id(self): def name(self): """Return the name of the sensor.""" ident = self._device['ident'] - + result = ident['deviceName'] if len(result) == 0: return ident['type']['value_localized'] + ' ' + self._ha_key @@ -86,8 +89,8 @@ def device_class(self): else: return 'problem' - async def async_update(self): + async def async_update(self): if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]: - _LOGGER.error('Miele device not found: {}'.format(self.device_id)) + _LOGGER.debug('Miele device not found: {}'.format(self.device_id)) else: self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id] diff --git a/miele/light.py b/custom_components/miele/light.py similarity index 86% rename from miele/light.py rename to custom_components/miele/light.py index d16e3f6..2ea4cdb 100644 --- a/miele/light.py +++ b/custom_components/miele/light.py @@ -3,7 +3,7 @@ from datetime import timedelta from homeassistant.helpers.entity import Entity -from homeassistant.components.light import Light +from homeassistant.components.light import LightEntity from custom_components.miele import DOMAIN as MIELE_DOMAIN, DATA_CLIENT, DATA_DEVICES @@ -13,11 +13,11 @@ ALL_DEVICES = [] -SUPPORTED_TYPES = [ 17, 18, 32, 33, 34, 68 ] +SUPPORTED_TYPES = [17, 18, 32, 33, 34, 68] + # pylint: disable=W0612 def setup_platform(hass, config, add_devices, discovery_info=None): - global ALL_DEVICES devices = hass.data[MIELE_DOMAIN][DATA_DEVICES] @@ -31,11 +31,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(light_devices) ALL_DEVICES = ALL_DEVICES + light_devices + def update_device_state(): for device in ALL_DEVICES: device.async_schedule_update_ha_state(True) -class MieleLight(Light): + +class MieleLight(LightEntity): def __init__(self, hass, device): self._hass = hass self._device = device @@ -55,7 +57,7 @@ def unique_id(self): def name(self): """Return the name of the light.""" ident = self._device['ident'] - + result = ident['deviceName'] if len(result) == 0: return ident['type']['value_localized'] @@ -65,24 +67,24 @@ def name(self): @property def is_on(self): """Return the state of the light.""" - return self._device['state']['light'] == 1 + return self._device['state']['light'] == 1 def turn_on(self, **kwargs): service_parameters = { 'device_id': self.device_id, - 'body': { 'light': 1 } + 'body': {'light': 1} } self._hass.services.call(MIELE_DOMAIN, 'action', service_parameters) def turn_off(self, **kwargs): service_parameters = { 'device_id': self.device_id, - 'body': { 'light': 2 } + 'body': {'light': 2} } self._hass.services.call(MIELE_DOMAIN, 'action', service_parameters) - async def async_update(self): + async def async_update(self): if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]: - _LOGGER.error('Miele device not found: {}'.format(self.device_id)) + _LOGGER.debug('Miele device not found: {}'.format(self.device_id)) else: self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id] diff --git a/custom_components/miele/manifest.json b/custom_components/miele/manifest.json new file mode 100644 index 0000000..eb9e5d3 --- /dev/null +++ b/custom_components/miele/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "miele", + "name": "Miele@home", + "documentation": "https://github.com/docbobo/home-assistant-miele", + "requirements": [ + "requests_oauthlib" + ], + "dependencies": [ + "http" + ], + "codeowners": ["docbobo, Kloknibor"] +} diff --git a/miele/miele_at_home.py b/custom_components/miele/miele_at_home.py similarity index 84% rename from miele/miele_at_home.py rename to custom_components/miele/miele_at_home.py index 61ff83a..cca92c0 100644 --- a/miele/miele_at_home.py +++ b/custom_components/miele/miele_at_home.py @@ -1,5 +1,7 @@ import json import logging +import functools + from datetime import timedelta @@ -12,20 +14,22 @@ class MieleClient(object): DEVICES_URL = 'https://api.mcs3.miele.com/v1/devices' ACTION_URL = 'https://api.mcs3.miele.com/v1/devices/{0}/actions' - def __init__(self, session): + def __init__(self, hass, session): self._session = session + self.hass = hass - def _get_devices_raw(self, lang): + async def _get_devices_raw(self, lang): _LOGGER.debug('Requesting Miele device update') try: - devices = self._session._session.get(MieleClient.DEVICES_URL, params={'language':lang}) + func = functools.partial(self._session._session.get, MieleClient.DEVICES_URL, params={'language':lang}) + devices = await self.hass.async_add_executor_job(func) if devices.status_code == 401: _LOGGER.info('Request unauthorized - attempting token refresh') if self._session.refresh_token(): - return self._get_devices_raw(lang) + return self._get_devices_raw(lang) if devices.status_code != 200: - _LOGGER.error('Failed to retrieve devices: {}'.format(devices.status_code)) + _LOGGER.debug('Failed to retrieve devices: {}'.format(devices.status_code)) return None return devices.json() @@ -34,8 +38,8 @@ def _get_devices_raw(self, lang): _LOGGER.error('Failed to retrieve Miele devices: {0}'.format(err)) return None - def get_devices(self, lang='en'): - home_devices = self._get_devices_raw(lang) + async def get_devices(self, lang='en'): + home_devices = await self._get_devices_raw(lang) if home_devices is None: return None @@ -43,7 +47,7 @@ def get_devices(self, lang='en'): for home_device in home_devices: result.append(home_devices[home_device]) - return result + return result def get_device(self, device_id, lang='en'): devices = self._get_devices_raw(lang) @@ -63,7 +67,7 @@ def action(self, device_id, body): if result.status_code == 401: _LOGGER.info('Request unauthorized - attempting token refresh') if self._session.refresh_token(): - return self.action(device_id, body) + return self.action(device_id, body) if result.status_code == 200: return result.json() @@ -95,8 +99,8 @@ def __init__(self, client_id, client_secret, redirect_uri, cache_path=None): self._session = OAuth2Session(self._client_id, auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL, - redirect_uri=redirect_uri, - token=self._token, + redirect_uri=redirect_uri, + token=self._token, token_updater=self._save_token) if self.authorized: @@ -109,8 +113,8 @@ def authorized(self): @property def authorization_url(self): return self._session.authorization_url(MieleOAuth.OAUTH_AUTHORIZE_URL, state='login')[0] - - + + def get_access_token(self, client_code): token = self._session.fetch_token( MieleOAuth.OAUTH_TOKEN_URL, @@ -121,13 +125,13 @@ def get_access_token(self, client_code): return token - def refresh_token(self): + async def refresh_token(self): body = 'client_id={}&client_secret={}&'.format(self._client_id, self._client_secret) - self._token = self._session.refresh_token(MieleOAuth.OAUTH_TOKEN_URL, + self._token = await self._session.refresh_token(MieleOAuth.OAUTH_TOKEN_URL, body=body, refresh_token=self._token['refresh_token']) self._save_token(self._token) - + def _get_cached_token(self): token = None if self._cache_path: @@ -139,7 +143,7 @@ def _get_cached_token(self): except IOError: pass - + return token def _save_token(self, token): diff --git a/miele/sensor.py b/custom_components/miele/sensor.py similarity index 77% rename from miele/sensor.py rename to custom_components/miele/sensor.py index ae0a6ea..9d10165 100644 --- a/miele/sensor.py +++ b/custom_components/miele/sensor.py @@ -107,7 +107,7 @@ def state(self): async def async_update(self): if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]: - _LOGGER.error('Miele device disappeared: {}'.format(self.device_id)) + _LOGGER.debug('Miele device disappeared: {}'.format(self.device_id)) else: self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id] @@ -142,17 +142,45 @@ def device_state_attributes(self): attributes['dryingStep'] = device_state['dryingStep']['value_localized'] attributes['rawDryingStep'] = device_state['dryingStep']['value_raw'] + if 'spinningSpeed' in device_state: + attributes['spinningSpeed'] = device_state['spinningSpeed']['value_localized'] + attributes['rawSpinningSpeed'] = device_state['spinningSpeed']['value_raw'] + if 'ventilationStep' in device_state: attributes['ventilationStep'] = device_state['ventilationStep']['value_localized'] attributes['rawVentilationStep'] = device_state['ventilationStep']['value_raw'] + if 'plateStep' in device_state: + plate_steps = 1 + for plateStep in device_state['plateStep']: + attributes['plateStep'+str(plate_steps)] = plateStep['value_localized'] + attributes['rawPlateStep'+str(plate_steps)] = plateStep['value_raw'] + plate_steps += 1 + + if 'ecoFeedback' in device_state and device_state['ecoFeedback'] is not None: + if 'currentWaterConsumption' in device_state['ecoFeedback']: + attributes['currentWaterConsumption'] = device_state['ecoFeedback']['currentWaterConsumption']['value'] + attributes['currentWaterConsumptionUnit'] = device_state['ecoFeedback']['currentWaterConsumption']['unit'] + if 'currentEnergyConsumption' in device_state['ecoFeedback']: + attributes['currentEnergyConsumption'] = device_state['ecoFeedback']['currentEnergyConsumption']['value'] + attributes['currentEnergyConsumptionUnit'] = device_state['ecoFeedback']['currentEnergyConsumption']['unit'] + if 'waterForecast' in device_state['ecoFeedback']: + attributes['waterForecast'] = device_state['ecoFeedback']['waterForecast'] + if 'energyForecast' in device_state['ecoFeedback']: + attributes['energyForecast'] = device_state['ecoFeedback']['energyForecast'] + # Programs will only be running of both remainingTime and elapsedTime indicate # a value > 0 if 'remainingTime' in device_state and 'elapsedTime' in device_state: remainingTime = _to_seconds(device_state['remainingTime']) elapsedTime = _to_seconds(device_state['elapsedTime']) - # Calculate progress + if 'startTime' in device_state: + startTime = _to_seconds(device_state['startTime']) + else: + startTime = 0 + + # Calculate progress if (elapsedTime + remainingTime) == 0: attributes['progress'] = None else: @@ -163,7 +191,15 @@ def device_state_attributes(self): attributes['finishTime'] = None else: now = datetime.now() - attributes['finishTime'] = (now + timedelta(seconds=remainingTime)).strftime('%H:%M') + attributes['finishTime'] = (now + timedelta(seconds=startTime) + timedelta(seconds=remainingTime)).strftime('%H:%M') + + # Calculate start time + if startTime == 0: + now = datetime.now() + attributes['kickoffTime'] = (now - timedelta(seconds=elapsedTime)).strftime('%H:%M') + else: + now = datetime.now() + attributes['kickoffTime'] = (now + timedelta(seconds=startTime)).strftime('%H:%M') return attributes @@ -233,6 +269,6 @@ def device_class(self): async def async_update(self): if not self.device_id in self._hass.data[MIELE_DOMAIN][DATA_DEVICES]: - _LOGGER.error(' Miele device disappeared: {}'.format(self.device_id)) + _LOGGER.debug(' Miele device disappeared: {}'.format(self.device_id)) else: self._device = self._hass.data[MIELE_DOMAIN][DATA_DEVICES][self.device_id] diff --git a/custom_components/miele/services.yaml b/custom_components/miele/services.yaml new file mode 100644 index 0000000..e69de29 diff --git a/info.md b/info.md new file mode 100644 index 0000000..87cf573 --- /dev/null +++ b/info.md @@ -0,0 +1,53 @@ +# Home Assistant support for Miele@home connected appliances + +## Introduction + +This project exposes Miele state information of appliances connected to a Miele user account. This is achieved by communicating with the Miele Cloud Service, which exposes both applicances connected to a Miele@home Gateway XGW3000, as well as those devices connected via WiFi Con@ct. + +## Prerequisite + +* A running version of [Home Assistant](https://home-assistant.io). While earlier versions may work, the custom component has been developed and tested with version 0.76.x. +* The ```requests_oauthlib``` library as part of your HA installation. Please install via ```pip3 install requests_oauthlib```. + For Hassbian you need to install this via : +``` +cd /srv/ +sudo chown homeassistant:homeassistant homeassistant +sudo su -s /bin/bash homeassistant +cd /srv/homeassistant +source bin/activate +pip3 install requests_oauthlib +``` + +* Following the [instructions on the Miele developer site](https://www.miele.com/developer/getinvolved.html), you need to request your personal ```ClientID``` and ```ClientSecret```. + +## Installation of the custom component + +* Copy the content of this repository into your ```custom_components``` folder, which is a subdirectory of your Home Assistant configuration directory. By default, this directory is located under ```~/.home-assistant```. The structure of the ```custom_components``` directory should look like this afterwards: + +``` +- miele + - __init__.py + - miele_at_home.py + - binary_sensor.py + - light.py + - sensor.py +``` + +* Enabled the new platform in your ```configuration.yaml```: + +``` +miele: + client_id: + client_secret: + lang: + cache_path: +``` + +* Restart Home Assistant. +* The Home Assistant Web UI will show you a UI to configure the Miele platform. Follow the instructions to log into the Miele Cloud Service. This will communicate back an authentication token that will be cached to communicate with the Cloud Service. + +Done. If you follow all the instructions, the Miele integration should be up and running. All Miele devices that you can see in your Mobile application should now be also visible in Home Assistant (miele.*). In addition, there will be a number of ```binary_sensors``` and ```sensors``` that can be used for automation. + +## Questions + +Please see the [Miele@home, miele@mobile component](https://community.home-assistant.io/t/miele-home-miele-mobile-component/64508) discussion thread on the Home Assistant community site. \ No newline at end of file