Home Assistant Unofficial Reference 2024.12.1
api.py
Go to the documentation of this file.
1 """API for fitbit bound to Home Assistant OAuth."""
2 
3 from abc import ABC, abstractmethod
4 from collections.abc import Callable
5 import logging
6 from typing import Any, cast
7 
8 from fitbit import Fitbit
9 from fitbit.exceptions import HTTPException, HTTPUnauthorized
10 from requests.exceptions import ConnectionError as RequestsConnectionError
11 
12 from homeassistant.const import CONF_ACCESS_TOKEN
13 from homeassistant.core import HomeAssistant
14 from homeassistant.helpers import config_entry_oauth2_flow
15 from homeassistant.util.unit_system import METRIC_SYSTEM
16 
17 from .const import FitbitUnitSystem
18 from .exceptions import FitbitApiException, FitbitAuthException
19 from .model import FitbitDevice, FitbitProfile
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 CONF_REFRESH_TOKEN = "refresh_token"
24 CONF_EXPIRES_AT = "expires_at"
25 
26 
27 class FitbitApi(ABC):
28  """Fitbit client library wrapper base class.
29 
30  This can be subclassed with different implementations for providing an access
31  token depending on the use case.
32  """
33 
34  def __init__(
35  self,
36  hass: HomeAssistant,
37  unit_system: FitbitUnitSystem | None = None,
38  ) -> None:
39  """Initialize Fitbit auth."""
40  self._hass_hass = hass
41  self._profile_profile: FitbitProfile | None = None
42  self._unit_system_unit_system = unit_system
43 
44  @abstractmethod
45  async def async_get_access_token(self) -> dict[str, Any]:
46  """Return a valid token dictionary for the Fitbit API."""
47 
48  async def _async_get_client(self) -> Fitbit:
49  """Get synchronous client library, called before each client request."""
50  # Always rely on Home Assistant's token update mechanism which refreshes
51  # the data in the configuration entry.
52  token = await self.async_get_access_token()
53  return Fitbit(
54  client_id=None,
55  client_secret=None,
56  access_token=token[CONF_ACCESS_TOKEN],
57  refresh_token=token[CONF_REFRESH_TOKEN],
58  expires_at=float(token[CONF_EXPIRES_AT]),
59  )
60 
61  async def async_get_user_profile(self) -> FitbitProfile:
62  """Return the user profile from the API."""
63  if self._profile_profile is None:
64  client = await self._async_get_client_async_get_client()
65  response: dict[str, Any] = await self._run(client.user_profile_get)
66  _LOGGER.debug("user_profile_get=%s", response)
67  profile = response["user"]
68  self._profile_profile = FitbitProfile(
69  encoded_id=profile["encodedId"],
70  display_name=profile["displayName"],
71  locale=profile.get("locale"),
72  )
73  return self._profile_profile
74 
75  async def async_get_unit_system(self) -> FitbitUnitSystem:
76  """Get the unit system to use when fetching timeseries.
77 
78  This is used in a couple ways. The first is to determine the request
79  header to use when talking to the fitbit API which changes the
80  units returned by the API. The second is to tell Home Assistant the
81  units set in sensor values for the values returned by the API.
82  """
83  if (
84  self._unit_system_unit_system is not None
85  and self._unit_system_unit_system != FitbitUnitSystem.LEGACY_DEFAULT
86  ):
87  return self._unit_system_unit_system
88  # Use units consistent with the account user profile or fallback to the
89  # home assistant unit settings.
90  profile = await self.async_get_user_profileasync_get_user_profile()
91  if profile.locale == FitbitUnitSystem.EN_GB:
92  return FitbitUnitSystem.EN_GB
93  if self._hass_hass.config.units is METRIC_SYSTEM:
94  return FitbitUnitSystem.METRIC
95  return FitbitUnitSystem.EN_US
96 
97  async def async_get_devices(self) -> list[FitbitDevice]:
98  """Return available devices."""
99  client = await self._async_get_client_async_get_client()
100  devices: list[dict[str, str]] = await self._run(client.get_devices)
101  _LOGGER.debug("get_devices=%s", devices)
102  return [
103  FitbitDevice(
104  id=device["id"],
105  device_version=device["deviceVersion"],
106  battery_level=int(device["batteryLevel"]),
107  battery=device["battery"],
108  type=device["type"],
109  )
110  for device in devices
111  ]
112 
113  async def async_get_latest_time_series(self, resource_type: str) -> dict[str, Any]:
114  """Return the most recent value from the time series for the specified resource type."""
115  client = await self._async_get_client_async_get_client()
116 
117  # Set request header based on the configured unit system
118  client.system = await self.async_get_unit_systemasync_get_unit_system()
119 
120  def _time_series() -> dict[str, Any]:
121  return cast(dict[str, Any], client.time_series(resource_type, period="7d"))
122 
123  response: dict[str, Any] = await self._run(_time_series)
124  _LOGGER.debug("time_series(%s)=%s", resource_type, response)
125  key = resource_type.replace("/", "-")
126  dated_results: list[dict[str, Any]] = response[key]
127  return dated_results[-1]
128 
129  async def _run[_T](self, func: Callable[[], _T]) -> _T:
130  """Run client command."""
131  try:
132  return await self._hass_hass.async_add_executor_job(func)
133  except RequestsConnectionError as err:
134  _LOGGER.debug("Connection error to fitbit API: %s", err)
135  raise FitbitApiException("Connection error to fitbit API") from err
136  except HTTPUnauthorized as err:
137  _LOGGER.debug("Unauthorized error from fitbit API: %s", err)
138  raise FitbitAuthException("Authentication error from fitbit API") from err
139  except HTTPException as err:
140  _LOGGER.debug("Error from fitbit API: %s", err)
141  raise FitbitApiException("Error from fitbit API") from err
142 
143 
145  """Provide fitbit authentication tied to an OAuth2 based config entry."""
146 
147  def __init__(
148  self,
149  hass: HomeAssistant,
150  oauth_session: config_entry_oauth2_flow.OAuth2Session,
151  unit_system: FitbitUnitSystem | None = None,
152  ) -> None:
153  """Initialize OAuthFitbitApi."""
154  super().__init__(hass, unit_system)
155  self._oauth_session_oauth_session = oauth_session
156 
157  async def async_get_access_token(self) -> dict[str, Any]:
158  """Return a valid access token for the Fitbit API."""
159  await self._oauth_session_oauth_session.async_ensure_token_valid()
160  return self._oauth_session_oauth_session.token
161 
162 
164  """Profile fitbit authentication before a ConfigEntry exists.
165 
166  This implementation directly provides the token without supporting refresh.
167  """
168 
169  def __init__(
170  self,
171  hass: HomeAssistant,
172  token: dict[str, Any],
173  ) -> None:
174  """Initialize ConfigFlowFitbitApi."""
175  super().__init__(hass)
176  self._token_token = token
177 
178  async def async_get_access_token(self) -> dict[str, Any]:
179  """Return the token for the Fitbit API."""
180  return self._token_token
None __init__(self, HomeAssistant hass, dict[str, Any] token)
Definition: api.py:173
FitbitProfile async_get_user_profile(self)
Definition: api.py:61
FitbitUnitSystem async_get_unit_system(self)
Definition: api.py:75
None __init__(self, HomeAssistant hass, FitbitUnitSystem|None unit_system=None)
Definition: api.py:38
dict[str, Any] async_get_latest_time_series(self, str resource_type)
Definition: api.py:113
dict[str, Any] async_get_access_token(self)
Definition: api.py:45
list[FitbitDevice] async_get_devices(self)
Definition: api.py:97
None __init__(self, HomeAssistant hass, config_entry_oauth2_flow.OAuth2Session oauth_session, FitbitUnitSystem|None unit_system=None)
Definition: api.py:152