From f5be9ef7fb43a5f14e34d2435c64499d7e48c82c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Feb 2020 00:13:54 -0600 Subject: [PATCH] Refresh the august access token when needed (#31735) * Refresh the august access token when needed. Currently august will stop working when the token expires about every six month. This resolves issue #23788 * Make refresh_access_token_if_needed private since we do not want additional callers * Add init --- CODEOWNERS | 1 + homeassistant/components/august/__init__.py | 35 +++++++++++++-- homeassistant/components/august/manifest.json | 2 +- requirements_test_all.txt | 3 ++ tests/components/august/__init__.py | 1 + tests/components/august/test_init.py | 44 +++++++++++++++++++ 6 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/components/august/__init__.py create mode 100644 tests/components/august/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 658fca1e8fc..48a23bba619 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs +homeassistant/components/august/* @bdraco homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index c6a0f90fcaa..c7b81646b42 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -136,7 +136,7 @@ def setup_august(hass, config, api, authenticator): if DOMAIN in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) - hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token) + hass.data[DATA_AUGUST] = AugustData(hass, api, authentication, authenticator) for component in AUGUST_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -193,11 +193,14 @@ def setup(hass, config): class AugustData: """August data object.""" - def __init__(self, hass, api, access_token): + def __init__(self, hass, api, authentication, authenticator): """Init August data object.""" self._hass = hass self._api = api - self._access_token = access_token + self._authenticator = authenticator + self._access_token = authentication.access_token + self._access_token_expires = authentication.access_token_expires + self._doorbells = self._api.get_doorbells(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or [] self._house_ids = set() @@ -227,6 +230,21 @@ class AugustData: """Return a list of locks.""" return self._locks + def _refresh_access_token_if_needed(self): + """Refresh the august access token if needed.""" + + if self._authenticator.should_refresh(): + refreshed_authentication = self._authenticator.refresh_access_token( + force=False + ) + _LOGGER.info( + "Refreshed august access token. The old token expired at %s, and the new token expires at %s", + self._access_token_expires, + refreshed_authentication.access_token_expires, + ) + self._access_token = refreshed_authentication.access_token + self._access_token_expires = refreshed_authentication.access_token_expires + def get_device_activities(self, device_id, *activity_types): """Return a list of activities.""" _LOGGER.debug("Getting device activities") @@ -245,6 +263,17 @@ class AugustData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): """Update data object with latest from August API.""" + + # This is the only place we refresh the api token + # in order to avoid multiple threads from doing it at the same time + # since there will only be one activity refresh at a time + # + # In the future when this module is converted to async we should + # use a lock to prevent all api calls while the token + # is being refreshed as this is a better solution + # + self._refresh_access_token_if_needed() + _LOGGER.debug("Start retrieving device activities") for house_id in self.house_ids: _LOGGER.debug("Updating device activity for house id %s", house_id) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index f74e20f38c2..fb5bb3ef3ef 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/august", "requirements": ["py-august==0.11.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bdraco"] } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 063c5c72f47..da710af4722 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -389,6 +389,9 @@ pure-python-adb==0.2.2.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 +# homeassistant.components.august +py-august==0.11.0 + # homeassistant.components.canary py-canary==0.5.0 diff --git a/tests/components/august/__init__.py b/tests/components/august/__init__.py new file mode 100644 index 00000000000..156b6170511 --- /dev/null +++ b/tests/components/august/__init__.py @@ -0,0 +1 @@ +"""Tests for the august component.""" diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py new file mode 100644 index 00000000000..e84df35b6b1 --- /dev/null +++ b/tests/components/august/test_init.py @@ -0,0 +1,44 @@ +"""The tests for the august platform.""" +from unittest.mock import MagicMock, PropertyMock + +from homeassistant.components import august + + +def _mock_august_authenticator(): + authenticator = MagicMock(name="august.authenticator") + authenticator.should_refresh = MagicMock( + name="august.authenticator.should_refresh", return_value=0 + ) + authenticator.refresh_access_token = MagicMock( + name="august.authenticator.refresh_access_token" + ) + return authenticator + + +def _mock_august_authentication(token_text, token_timestamp): + authentication = MagicMock(name="august.authentication") + type(authentication).access_token = PropertyMock(return_value=token_text) + type(authentication).access_token_expires = PropertyMock( + return_value=token_timestamp + ) + return authentication + + +def test__refresh_access_token(): + """Set up things to be run when tests are started.""" + authentication = _mock_august_authentication("original_token", 1234) + authenticator = _mock_august_authenticator() + data = august.AugustData( + MagicMock(name="hass"), MagicMock(name="api"), authentication, authenticator + ) + data._refresh_access_token_if_needed() + authenticator.refresh_access_token.assert_not_called() + + authenticator.should_refresh.return_value = 1 + authenticator.refresh_access_token.return_value = _mock_august_authentication( + "new_token", 5678 + ) + data._refresh_access_token_if_needed() + authenticator.refresh_access_token.assert_called() + assert data._access_token == "new_token" + assert data._access_token_expires == 5678