From 822251a642dfc47448ec821d6f8da2ddf585f4c1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 26 Sep 2023 07:50:42 -0700 Subject: [PATCH] Update fitbit client to use asyncio (#100933) --- homeassistant/components/fitbit/api.py | 29 +++++++++++++++-------- homeassistant/components/fitbit/sensor.py | 23 +++++++++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fitbit/api.py b/homeassistant/components/fitbit/api.py index 1b58d26e286..bf287471292 100644 --- a/homeassistant/components/fitbit/api.py +++ b/homeassistant/components/fitbit/api.py @@ -1,7 +1,7 @@ """API for fitbit bound to Home Assistant OAuth.""" import logging -from typing import Any +from typing import Any, cast from fitbit import Fitbit @@ -34,10 +34,12 @@ class FitbitApi: """Property to expose the underlying client library.""" return self._client - def get_user_profile(self) -> FitbitProfile: + async def async_get_user_profile(self) -> FitbitProfile: """Return the user profile from the API.""" if self._profile is None: - response: dict[str, Any] = self._client.user_profile_get() + response: dict[str, Any] = await self._hass.async_add_executor_job( + self._client.user_profile_get + ) _LOGGER.debug("user_profile_get=%s", response) profile = response["user"] self._profile = FitbitProfile( @@ -47,7 +49,7 @@ class FitbitApi: ) return self._profile - def get_unit_system(self) -> FitbitUnitSystem: + async def async_get_unit_system(self) -> FitbitUnitSystem: """Get the unit system to use when fetching timeseries. This is used in a couple ways. The first is to determine the request @@ -62,16 +64,18 @@ class FitbitApi: return self._unit_system # Use units consistent with the account user profile or fallback to the # home assistant unit settings. - profile = self.get_user_profile() + profile = await self.async_get_user_profile() if profile.locale == FitbitUnitSystem.EN_GB: return FitbitUnitSystem.EN_GB if self._hass.config.units is METRIC_SYSTEM: return FitbitUnitSystem.METRIC return FitbitUnitSystem.EN_US - def get_devices(self) -> list[FitbitDevice]: + async def async_get_devices(self) -> list[FitbitDevice]: """Return available devices.""" - devices: list[dict[str, str]] = self._client.get_devices() + devices: list[dict[str, str]] = await self._hass.async_add_executor_job( + self._client.get_devices + ) _LOGGER.debug("get_devices=%s", devices) return [ FitbitDevice( @@ -84,13 +88,18 @@ class FitbitApi: for device in devices ] - def get_latest_time_series(self, resource_type: str) -> dict[str, Any]: + async def async_get_latest_time_series(self, resource_type: str) -> dict[str, Any]: """Return the most recent value from the time series for the specified resource type.""" # Set request header based on the configured unit system - self._client.system = self.get_unit_system() + self._client.system = await self.async_get_unit_system() - response: dict[str, Any] = self._client.time_series(resource_type, period="7d") + def _time_series() -> dict[str, Any]: + return cast( + dict[str, Any], self._client.time_series(resource_type, period="7d") + ) + + response: dict[str, Any] = await self._hass.async_add_executor_job(_time_series) _LOGGER.debug("time_series(%s)=%s", resource_type, response) key = resource_type.replace("/", "-") dated_results: list[dict[str, Any]] = response[key] diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 653a4ee2508..e08f56e0e34 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,6 +1,7 @@ """Support for the Fitbit API.""" from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import dataclass import datetime @@ -518,8 +519,12 @@ def setup_platform( authd_client.client.refresh_token() api = FitbitApi(hass, authd_client, config[CONF_UNIT_SYSTEM]) - user_profile = api.get_user_profile() - unit_system = api.get_unit_system() + user_profile = asyncio.run_coroutine_threadsafe( + api.async_get_user_profile(), hass.loop + ).result() + unit_system = asyncio.run_coroutine_threadsafe( + api.async_get_unit_system(), hass.loop + ).result() clock_format = config[CONF_CLOCK_FORMAT] monitored_resources = config[CONF_MONITORED_RESOURCES] @@ -539,7 +544,10 @@ def setup_platform( if description.key in monitored_resources ] if "devices/battery" in monitored_resources: - devices = api.get_devices() + devices = asyncio.run_coroutine_threadsafe( + api.async_get_devices(), + hass.loop, + ).result() entities.extend( [ FitbitSensor( @@ -708,21 +716,24 @@ class FitbitSensor(SensorEntity): return attrs - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data from the Fitbit API and update the states.""" resource_type = self.entity_description.key if resource_type == "devices/battery" and self.device is not None: device_id = self.device.id - registered_devs: list[FitbitDevice] = self.api.get_devices() + registered_devs: list[FitbitDevice] = await self.api.async_get_devices() self.device = next( device for device in registered_devs if device.id == device_id ) self._attr_native_value = self.device.battery else: - result = self.api.get_latest_time_series(resource_type) + result = await self.api.async_get_latest_time_series(resource_type) self._attr_native_value = self.entity_description.value_fn(result) + self.hass.async_add_executor_job(self._update_token) + + def _update_token(self) -> None: token = self.api.client.client.session.token config_contents = { ATTR_ACCESS_TOKEN: token.get("access_token"),