From f7b4f6ef2cf5f7346bec68550d07d5f12cc1e848 Mon Sep 17 00:00:00 2001 From: Andrey Khrolenok Date: Mon, 5 Apr 2021 13:16:22 +0300 Subject: [PATCH] Fix #28 --- custom_components/jq300/__init__.py | 5 ++- custom_components/jq300/api.py | 58 ++++++++++++----------------- tests/conftest.py | 12 ++---- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/custom_components/jq300/__init__.py b/custom_components/jq300/__init__.py index e390310..e9f2890 100644 --- a/custom_components/jq300/__init__.py +++ b/custom_components/jq300/__init__.py @@ -13,6 +13,7 @@ import logging from datetime import timedelta +import async_timeout import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -31,6 +32,7 @@ DOMAIN, PLATFORMS, STARTUP_MESSAGE, + UPDATE_TIMEOUT, ) from .util import mask_email @@ -95,7 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - devices = await account.async_update_devices_or_timeout() + with async_timeout.timeout(UPDATE_TIMEOUT): + devices = await hass.async_add_executor_job(account.update_devices) except TimeoutError as exc: raise ConfigEntryNotReady from exc diff --git a/custom_components/jq300/api.py b/custom_components/jq300/api.py index 52edf91..6315a59 100644 --- a/custom_components/jq300/api.py +++ b/custom_components/jq300/api.py @@ -13,7 +13,7 @@ import json import logging from datetime import timedelta -from time import monotonic, sleep +from time import monotonic from typing import Any, Dict, Optional from urllib.parse import urlparse @@ -184,10 +184,16 @@ def _get_url(self, query_type, function: str, extra_params=None) -> str: url = self._add_url_params(url, extra_params) return url - # pylint: disable=too-many-return-statements,too-many-branches async def _async_query( self, query_type, function: str, extra_params=None ) -> Optional[dict]: + """Query data from cloud.""" + return await self.hass.async_add_executor_job( + self._query, query_type, function, extra_params + ) + + # pylint: disable=too-many-return-statements,too-many-branches + def _query(self, query_type, function: str, extra_params=None) -> Optional[dict]: """Query data from cloud.""" url = self._get_url(query_type, function) _LOGGER.debug("Requesting URL %s", url) @@ -252,7 +258,7 @@ def is_connected(self) -> bool: """Return True if connected to account.""" return self.params["uid"] > 0 - async def async_connect(self, force: bool = False) -> bool: + def connect(self, force: bool = False) -> bool: """(Re)Connect to account and return connection status.""" if not force and self.params["uid"] > 0: return True @@ -261,7 +267,7 @@ async def async_connect(self, force: bool = False) -> bool: self.params["uid"] = -1000 self.params["safeToken"] = "anonymous" - ret = await self._async_query( + ret = self._query( QUERY_TYPE_API, "loginByEmail", extra_params={ @@ -272,7 +278,7 @@ async def async_connect(self, force: bool = False) -> bool: }, ) if not ret: - return await self.async_connect(True) if not force else False + return self.connect(True) if not force else False self.params["uid"] = ret["uid"] self.params["safeToken"] = ret["safeToken"] @@ -347,6 +353,7 @@ def _mqtt_process_message(self, message: dict): int(dt_util.now().timestamp()), json.loads(message["content"]), ) + self.devices[device_id]["onlinets"] = monotonic() elif message["type"] == "C": if self.devices.get(device_id) is None: @@ -362,8 +369,7 @@ def _mqtt_process_message(self, message: dict): def _get_devices_mqtt_topics(self, device_ids: list) -> list: if not self.devices: - self.hass.add_job(self.async_update_devices()) - sleep(1) + self.update_devices() if not self.devices: return [] @@ -400,11 +406,9 @@ def devices(self) -> dict: """Get available devices.""" return self._devices - async def async_update_devices( - self, force=False - ) -> Optional[Dict[int, Dict[str, Any]]]: - """Update available devices.""" - if not await self.async_connect(): + def update_devices(self, force=False) -> Optional[Dict[int, Dict[str, Any]]]: + """Update available devices from cloud.""" + if not self.connect(): _LOGGER.error("Can't connect to cloud.") return None @@ -413,7 +417,7 @@ async def async_update_devices( _LOGGER.debug("Updating devices list for account %s", self.name_secure) - ret = await self._async_query( + ret = self._query( QUERY_TYPE_API, "deviceManager", extra_params={ @@ -423,7 +427,7 @@ async def async_update_devices( }, ) if not ret: - return await self.async_update_devices(True) if not force else None + return self.update_devices(True) if not force else None tstamp = int(dt_util.now().timestamp() * 1000) for dev in ret["deviceInfoBodyList"]: @@ -436,24 +440,6 @@ async def async_update_devices( return self._devices - async def async_update_devices_or_timeout(self, timeout=UPDATE_TIMEOUT): - """Get available devices list from cloud.""" - start = monotonic() - try: - with async_timeout.timeout(timeout): - return await self.async_update_devices() - - except TimeoutError as exc: - _LOGGER.error("Timeout fetching %s devices list", self.name_secure) - raise exc - - finally: - _LOGGER.debug( - "Finished fetching %s devices list in %.3f seconds", - self.name_secure, - monotonic() - start, - ) - def device_available(self, device_id) -> bool: """Return True if device is available.""" dev = self.devices.get(device_id, {}) @@ -493,8 +479,12 @@ def _extract_sensors_data(self, device_id, ts_now: int, sensors: dict): self._sensors[device_id][ts_now] = res self._sensors_raw[device_id] = res - @Throttle(timedelta(minutes=10)) async def async_update_sensors(self): + """Update current states of all active devices for account.""" + return await self.hass.async_add_executor_job(self.update_sensors) + + @Throttle(timedelta(minutes=10)) + def update_sensors(self): """Update current states of all active devices for account.""" _LOGGER.debug("Updating sensors state for account %s", self.name_secure) @@ -504,7 +494,7 @@ async def async_update_sensors(self): if self.get_sensors_raw(device_id) is not None: continue - ret = await self._async_query( + ret = self._query( QUERY_TYPE_DEVICE, "list", extra_params={ diff --git a/tests/conftest.py b/tests/conftest.py index ec8f6c8..e3b793e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -54,18 +54,14 @@ def bypass_get_data_fixture(): "test_device": {"pt_name": "test_name"}, "another_device": {"pt_name": "another_name"}, } - with patch.object( - Jq300Account, "async_update_devices_or_timeout", return_value=res - ): + with patch.object(Jq300Account, "update_devices", return_value=res): yield -# In this fixture, we are forcing calls to async_get_data to raise an Exception. This is useful -# for exception handling. +# In this fixture, we are forcing calls to async_get_data to raise an Exception. +# This is useful for exception handling. @pytest.fixture(name="error_on_get_data") def error_get_data_fixture(): """Simulate error when retrieving data from API.""" - with patch.object( - Jq300Account, "async_update_devices_or_timeout", side_effect=TimeoutError - ): + with patch.object(Jq300Account, "update_devices", side_effect=TimeoutError): yield