Update Fitbit integration to allow UI based configuration (#100897)
* Cleanup fitbit sensor API parsing * Remove API code that is not used yet * Configuration flow for fitbit * Code cleanup after manual review * Streamline code for review * Use scopes to determine which entities to enable * Use set for entity comparisons * Apply fitbit string pr feedback * Improve fitbit configuration flow error handling * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Fix typo in more places * Revert typing import * Revert custom domain back to default * Add additional config flow tests * Add breaks_in_ha_version to repair issues * Update homeassistant/components/fitbit/api.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Increase test coverage for token refresh success case * Add breaks_in_ha_version for sensor issue * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Simplify translations, issue keys, and token refresh * Config flow test improvements * Simplify repair issue creation on fitbit import * Remove unused strings --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
fe30c019b6
commit
bd2fee289d
15 changed files with 1188 additions and 302 deletions
|
@ -1,11 +1,14 @@
|
|||
"""API for fitbit bound to Home Assistant OAuth."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from fitbit import Fitbit
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import FitbitUnitSystem
|
||||
|
@ -13,32 +16,50 @@ from .model import FitbitDevice, FitbitProfile
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
CONF_EXPIRES_AT = "expires_at"
|
||||
|
||||
class FitbitApi:
|
||||
"""Fitbit client library wrapper base class."""
|
||||
|
||||
class FitbitApi(ABC):
|
||||
"""Fitbit client library wrapper base class.
|
||||
|
||||
This can be subclassed with different implementations for providing an access
|
||||
token depending on the use case.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: Fitbit,
|
||||
unit_system: FitbitUnitSystem | None = None,
|
||||
) -> None:
|
||||
"""Initialize Fitbit auth."""
|
||||
self._hass = hass
|
||||
self._profile: FitbitProfile | None = None
|
||||
self._client = client
|
||||
self._unit_system = unit_system
|
||||
|
||||
@property
|
||||
def client(self) -> Fitbit:
|
||||
"""Property to expose the underlying client library."""
|
||||
return self._client
|
||||
@abstractmethod
|
||||
async def async_get_access_token(self) -> dict[str, Any]:
|
||||
"""Return a valid token dictionary for the Fitbit API."""
|
||||
|
||||
async def _async_get_client(self) -> Fitbit:
|
||||
"""Get synchronous client library, called before each client request."""
|
||||
# Always rely on Home Assistant's token update mechanism which refreshes
|
||||
# the data in the configuration entry.
|
||||
token = await self.async_get_access_token()
|
||||
return Fitbit(
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
access_token=token[CONF_ACCESS_TOKEN],
|
||||
refresh_token=token[CONF_REFRESH_TOKEN],
|
||||
expires_at=float(token[CONF_EXPIRES_AT]),
|
||||
)
|
||||
|
||||
async def async_get_user_profile(self) -> FitbitProfile:
|
||||
"""Return the user profile from the API."""
|
||||
if self._profile is None:
|
||||
client = await self._async_get_client()
|
||||
response: dict[str, Any] = await self._hass.async_add_executor_job(
|
||||
self._client.user_profile_get
|
||||
client.user_profile_get
|
||||
)
|
||||
_LOGGER.debug("user_profile_get=%s", response)
|
||||
profile = response["user"]
|
||||
|
@ -73,8 +94,9 @@ class FitbitApi:
|
|||
|
||||
async def async_get_devices(self) -> list[FitbitDevice]:
|
||||
"""Return available devices."""
|
||||
client = await self._async_get_client()
|
||||
devices: list[dict[str, str]] = await self._hass.async_add_executor_job(
|
||||
self._client.get_devices
|
||||
client.get_devices
|
||||
)
|
||||
_LOGGER.debug("get_devices=%s", devices)
|
||||
return [
|
||||
|
@ -90,17 +112,56 @@ class FitbitApi:
|
|||
|
||||
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."""
|
||||
client = await self._async_get_client()
|
||||
|
||||
# Set request header based on the configured unit system
|
||||
self._client.system = await self.async_get_unit_system()
|
||||
client.system = await self.async_get_unit_system()
|
||||
|
||||
def _time_series() -> dict[str, Any]:
|
||||
return cast(
|
||||
dict[str, Any], self._client.time_series(resource_type, period="7d")
|
||||
)
|
||||
return cast(dict[str, Any], 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]
|
||||
return dated_results[-1]
|
||||
|
||||
|
||||
class OAuthFitbitApi(FitbitApi):
|
||||
"""Provide fitbit authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
unit_system: FitbitUnitSystem | None = None,
|
||||
) -> None:
|
||||
"""Initialize OAuthFitbitApi."""
|
||||
super().__init__(hass, unit_system)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> dict[str, Any]:
|
||||
"""Return a valid access token for the Fitbit API."""
|
||||
if not self._oauth_session.valid_token:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
return self._oauth_session.token
|
||||
|
||||
|
||||
class ConfigFlowFitbitApi(FitbitApi):
|
||||
"""Profile fitbit authentication before a ConfigEntry exists.
|
||||
|
||||
This implementation directly provides the token without supporting refresh.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
token: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize ConfigFlowFitbitApi."""
|
||||
super().__init__(hass)
|
||||
self._token = token
|
||||
|
||||
async def async_get_access_token(self) -> dict[str, Any]:
|
||||
"""Return the token for the Fitbit API."""
|
||||
return self._token
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue