Source code for aiohomeconnect.cli.client

"""Provide a CLI client for Home Connect API."""

from __future__ import annotations

import asyncio
import contextlib
import json
from pathlib import Path
import time
from typing import Any

from authlib.integrations.httpx_client import AsyncOAuth2Client
from httpx import AsyncClient

from aiohomeconnect.client import AbstractAuth, Client
from aiohomeconnect.const import API_ENDPOINT, OAUTH2_AUTHORIZE, OAUTH2_TOKEN

TOKEN_FILE = "token.json"  # noqa: S105
TOKEN_EXPIRES_MARGIN = 20


[docs] class CLIClient(Client): """Represent a CLI client for Home Connect API.""" def __init__( self, client_id: str, client_secret: str, redirect_uri: str | None = None, scope: str | None = None, ) -> None: """Initialize the client.""" super().__init__( Auth( AsyncClient(), API_ENDPOINT, TokenManager( client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri, scope=scope, ), ), )
[docs] class Auth(AbstractAuth): """Implement the authentication.""" def __init__( self, httpx_client: AsyncClient, host: str, token_manager: TokenManager, ) -> None: """Initialize the auth.""" super().__init__(httpx_client, host) self.token_manager = token_manager
[docs] async def async_get_access_token(self) -> str: """Return a valid access token.""" if self.token_manager.access_token is None: await self.token_manager.load_access_token() if self.token_manager.access_token is None: raise ValueError("No access token available") if self.token_manager.is_token_valid(): return self.token_manager.access_token await self.token_manager.refresh_access_token() await self.token_manager.save_access_token() return self.token_manager.access_token
[docs] class TokenManager: """Manage the tokens for authentication.""" def __init__( self, client_id: str, client_secret: str, redirect_uri: str | None = None, scope: str | None = None, ) -> None: """Initialize the token manager.""" self.access_token: str | None = None self._refresh_token: str | None = None self._state: str | None = None self._token: dict[str, Any] = {} self._client = AsyncOAuth2Client( client_id, client_secret, scope=scope, redirect_uri=redirect_uri, )
[docs] async def create_authorization_url(self) -> str: """Create the authorization URL.""" uri, self._state = self._client.create_authorization_url( OAUTH2_AUTHORIZE, ) return uri
[docs] def is_token_valid(self) -> bool: """Check if the token is valid.""" return self._token["expires_at"] > time.time() + TOKEN_EXPIRES_MARGIN
[docs] async def fetch_access_token(self, code: str) -> dict[str, Any]: """Fetch the access token.""" token = self._token = await self._client.fetch_token( OAUTH2_TOKEN, code=code, grant_type="authorization_code", state=self._state, ) self._validate_token() self.access_token = token["access_token"] await self.save_access_token() return token
[docs] async def load_access_token(self) -> None: """Load the access token.""" await asyncio.to_thread(self._load_access_token) self.access_token = self._token.get("access_token") self._refresh_token = self._token.get("refresh_token")
[docs] async def refresh_access_token(self) -> None: """Refresh the access token.""" token = self._token = await self._client.refresh_token( OAUTH2_TOKEN, refresh_token=self._refresh_token, client_id=self._client.client_id, client_secret=self._client.client_secret, ) self._validate_token() self.access_token = token["access_token"] self._refresh_token = token["refresh_token"]
[docs] async def save_access_token(self) -> None: """Save the access token.""" await asyncio.to_thread(self._save_access_token)
def _load_access_token(self) -> None: """Load the access token.""" with contextlib.suppress(FileNotFoundError): self._token = json.loads(Path(TOKEN_FILE).read_text(encoding="utf-8")) def _save_access_token(self) -> None: """Save the access token.""" Path(TOKEN_FILE).write_text(json.dumps(self._token, indent=2), encoding="utf-8") def _validate_token(self) -> None: """Validate the token.""" token = self._token token["expires_in"] = int(token["expires_in"]) token["expires_at"] = time.time() + token["expires_in"]