From 1972140a2a130bf83c926e39f45aa2254a636d8e Mon Sep 17 00:00:00 2001 From: Robin Kolk Date: Thu, 7 Apr 2022 13:51:30 +0200 Subject: [PATCH] Moving from dev to stable (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Check for cached consumption first If a Miele device is temporarily disconnected from the Miele cloud while running, the Miele API returns a "not connected" state for the device. If the "not running" check is done first, it causes the energy/water consumption to be reset to 0. This in turn means that the statistics generated by Home Assistant will be messed up, it will look like more energy/water was consumed than in reality. * Fix missing ecoFeedback object * fix the coroutine error (#108) * Bump version * Switching back to manual versioning * fix the coroutine error * warn if unable to delete the cached token check if authorized before creating a new session Co-authored-by: sebltm Co-authored-by: kloknibor Co-authored-by: Sebastian Lövdahl Co-authored-by: mundschenk_at Co-authored-by: Sébastien Michel <26392528+sebltm@users.noreply.github.com> Co-authored-by: kloknibor --- custom_components/miele/__init__.py | 2 + custom_components/miele/miele_at_home.py | 57 +++++++++++++++++++----- custom_components/miele/sensor.py | 25 ++++++++--- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/custom_components/miele/__init__.py b/custom_components/miele/__init__.py index 6602664..c6f6b8b 100644 --- a/custom_components/miele/__init__.py +++ b/custom_components/miele/__init__.py @@ -85,6 +85,7 @@ async def miele_configuration_callback(callback_data): await async_setup(hass, config) + _LOGGER.info("Requesting authorization...") configurator = hass.components.configurator _CONFIGURING[DOMAIN] = configurator.async_request_config( DEFAULT_NAME, @@ -127,6 +128,7 @@ async def async_setup(hass, config): CONF_CACHE_PATH, hass.config.path(STORAGE_DIR, f".miele-token-cache") ) hass.data[DOMAIN][DATA_OAUTH] = MieleOAuth( + hass, config[DOMAIN].get(CONF_CLIENT_ID), config[DOMAIN].get(CONF_CLIENT_SECRET), redirect_uri=callback_url, diff --git a/custom_components/miele/miele_at_home.py b/custom_components/miele/miele_at_home.py index 81bccee..dae8d51 100644 --- a/custom_components/miele/miele_at_home.py +++ b/custom_components/miele/miele_at_home.py @@ -1,6 +1,7 @@ import functools import json import logging +import os from datetime import timedelta from requests.exceptions import ConnectionError @@ -28,8 +29,8 @@ async def _get_devices_raw(self, 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) + if await self._session.refresh_token(self.hass): + return await self._get_devices_raw(lang) if devices.status_code != 200: _LOGGER.debug( @@ -77,8 +78,14 @@ async def action(self, device_id, body): result = await self.hass.async_add_executor_job(func) if result.status_code == 401: _LOGGER.info("Request unauthorized - attempting token refresh") - if self._session.refresh_token(): - return self.action(device_id, body) + + if await self._session.refresh_token(self.hass): + if self._session.authorized: + return self.action(device_id, body) + else: + self._session._delete_token() + self._session.new_session() + return self.action(device_id, body) if result.status_code == 200: return result.json() @@ -105,10 +112,11 @@ class MieleOAuth(object): OAUTH_AUTHORIZE_URL = "https://api.mcs3.miele.com/thirdparty/login" OAUTH_TOKEN_URL = "https://api.mcs3.miele.com/thirdparty/token" - def __init__(self, client_id, client_secret, redirect_uri, cache_path=None): + def __init__(self, hass, client_id, client_secret, redirect_uri, cache_path=None): self._client_id = client_id self._client_secret = client_secret self._cache_path = cache_path + self._redirect_uri = redirect_uri self._token = self._get_cached_token() @@ -127,7 +135,7 @@ def __init__(self, client_id, client_secret, redirect_uri, cache_path=None): ) if self.authorized: - self.refresh_token() + self.refresh_token(hass) @property def authorized(self): @@ -150,17 +158,23 @@ def get_access_token(self, client_code): return token - async def refresh_token(self): + async def refresh_token(self, hass): body = "client_id={}&client_secret={}&".format( self._client_id, self._client_secret ) - self._token = await self._session.refresh_token( + self._token = await hass.async_add_executor_job( + self.sync_refresh_token, MieleOAuth.OAUTH_TOKEN_URL, - body=body, - refresh_token=self._token["refresh_token"], + body, + self._token["refresh_token"], ) self._save_token(self._token) + def sync_refresh_token(self, token_url, body, refresh_token): + return self._session.refresh_token( + token_url, body=body, refresh_token=refresh_token + ) + def _get_cached_token(self): token = None if self._cache_path: @@ -175,6 +189,29 @@ def _get_cached_token(self): return token + def _delete_token(self): + if self._cache_path: + try: + os.remove(self._cache_path) + + except IOError: + _LOGGER.warn("Unable to delete cached token") + + self._token = None + + def _new_session(self, redirect_uri): + self._session = OAuth2Session( + self._client_id, + auto_refresh_url=MieleOAuth.OAUTH_TOKEN_URL, + redirect_uri=self._redirect_uri, + token=self._token, + token_updater=self._save_token, + auto_refresh_kwargs=self._extra, + ) + + if self.authorized: + self.refresh_token() + def _save_token(self, token): _LOGGER.debug("trying to save new token") if self._cache_path: diff --git a/custom_components/miele/sensor.py b/custom_components/miele/sensor.py index a22d884..2387edf 100644 --- a/custom_components/miele/sensor.py +++ b/custom_components/miele/sensor.py @@ -631,13 +631,26 @@ def state(self): self._cached_consumption = -1 return 0 - if self._cached_consumption >= 0: - if ( - "ecoFeedback" not in device_state - or device_state["ecoFeedback"] is None - or device_status_value == STATUS_NOT_CONNECTED - ): + # Sometimes the Miele API seems to return a null ecoFeedback + # object even though the Miele device is running. Or if the the + # Miele device has lost the connection to the Miele cloud, the + # status is "not connected". Either way, we need to return the + # last known value until the API starts returning something + # sane again, otherwise the statistics generated from this + # sensor would be messed up. + if ( + "ecoFeedback" not in device_state + or device_state["ecoFeedback"] is None + or device_status_value == STATUS_NOT_CONNECTED + ): + if self._cached_consumption > 0: return self._cached_consumption + else: + return 0 + + if not _is_running(device_status_value): + self._cached_consumption = -1 + return 0 consumption = 0 if self._key == "energyConsumption":