Home Assistant Unofficial Reference 2024.12.1
api.py
Go to the documentation of this file.
1 """API for Google Nest Device Access bound to Home Assistant OAuth."""
2 
3 from __future__ import annotations
4 
5 import datetime
6 import logging
7 from typing import cast
8 
9 from aiohttp import ClientSession
10 from google.oauth2.credentials import Credentials
11 from google_nest_sdm.admin_client import PUBSUB_API_HOST, AdminClient
12 from google_nest_sdm.auth import AbstractAuth
13 from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
14 
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.core import HomeAssistant
17 from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
18 
19 from .const import (
20  API_URL,
21  CONF_PROJECT_ID,
22  CONF_SUBSCRIBER_ID,
23  CONF_SUBSCRIPTION_NAME,
24  OAUTH2_TOKEN,
25  SDM_SCOPES,
26 )
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 
31 class AsyncConfigEntryAuth(AbstractAuth):
32  """Provide Google Nest Device Access authentication tied to an OAuth2 based config entry."""
33 
34  def __init__(
35  self,
36  websession: ClientSession,
37  oauth_session: config_entry_oauth2_flow.OAuth2Session,
38  client_id: str,
39  client_secret: str,
40  ) -> None:
41  """Initialize Google Nest Device Access auth."""
42  super().__init__(websession, API_URL)
43  self._oauth_session_oauth_session = oauth_session
44  self._client_id_client_id = client_id
45  self._client_secret_client_secret = client_secret
46 
47  async def async_get_access_token(self) -> str:
48  """Return a valid access token for SDM API."""
49  await self._oauth_session_oauth_session.async_ensure_token_valid()
50  return cast(str, self._oauth_session_oauth_session.token["access_token"])
51 
52  async def async_get_creds(self) -> Credentials:
53  """Return an OAuth credential for Pub/Sub Subscriber."""
54  # We don't have a way for Home Assistant to refresh creds on behalf
55  # of the google pub/sub subscriber. Instead, build a full
56  # Credentials object with enough information for the subscriber to
57  # handle this on its own. We purposely don't refresh the token here
58  # even when it is expired to fully hand off this responsibility and
59  # know it is working at startup (then if not, fail loudly).
60  token = self._oauth_session_oauth_session.token
61  creds = Credentials( # type: ignore[no-untyped-call]
62  token=token["access_token"],
63  refresh_token=token["refresh_token"],
64  token_uri=OAUTH2_TOKEN,
65  client_id=self._client_id_client_id,
66  client_secret=self._client_secret_client_secret,
67  scopes=SDM_SCOPES,
68  )
69  creds.expiry = datetime.datetime.fromtimestamp(token["expires_at"])
70  return creds
71 
72 
73 class AccessTokenAuthImpl(AbstractAuth):
74  """Authentication implementation used during config flow, without refresh.
75 
76  This exists to allow the config flow to use the API before it has fully
77  created a config entry required by OAuth2Session. This does not support
78  refreshing tokens, which is fine since it should have been just created.
79  """
80 
81  def __init__(
82  self,
83  websession: ClientSession,
84  access_token: str,
85  host: str,
86  ) -> None:
87  """Init the Nest client library auth implementation."""
88  super().__init__(websession, host)
89  self._access_token_access_token = access_token
90 
91  async def async_get_access_token(self) -> str:
92  """Return the access token."""
93  return self._access_token_access_token
94 
95  async def async_get_creds(self) -> Credentials:
96  """Return an OAuth credential for Pub/Sub Subscriber."""
97  return Credentials( # type: ignore[no-untyped-call]
98  token=self._access_token_access_token,
99  token_uri=OAUTH2_TOKEN,
100  scopes=SDM_SCOPES,
101  )
102 
103 
104 async def new_subscriber(
105  hass: HomeAssistant, entry: ConfigEntry
106 ) -> GoogleNestSubscriber | None:
107  """Create a GoogleNestSubscriber."""
108  implementation = (
109  await config_entry_oauth2_flow.async_get_config_entry_implementation(
110  hass, entry
111  )
112  )
113  if not isinstance(
114  implementation, config_entry_oauth2_flow.LocalOAuth2Implementation
115  ):
116  raise TypeError(f"Unexpected auth implementation {implementation}")
117  if (subscription_name := entry.data.get(CONF_SUBSCRIPTION_NAME)) is None:
118  subscription_name = entry.data[CONF_SUBSCRIBER_ID]
119  auth = AsyncConfigEntryAuth(
120  aiohttp_client.async_get_clientsession(hass),
121  config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation),
122  implementation.client_id,
123  implementation.client_secret,
124  )
125  return GoogleNestSubscriber(auth, entry.data[CONF_PROJECT_ID], subscription_name)
126 
127 
129  hass: HomeAssistant,
130  access_token: str,
131  project_id: str,
132  subscription_name: str,
133 ) -> GoogleNestSubscriber:
134  """Create a GoogleNestSubscriber with an access token."""
135  return GoogleNestSubscriber(
137  aiohttp_client.async_get_clientsession(hass),
138  access_token,
139  API_URL,
140  ),
141  project_id,
142  subscription_name,
143  )
144 
145 
147  hass: HomeAssistant,
148  access_token: str,
149  cloud_project_id: str,
150 ) -> AdminClient:
151  """Create a Nest AdminClient with an access token."""
152  return AdminClient(
153  auth=AccessTokenAuthImpl(
154  aiohttp_client.async_get_clientsession(hass),
155  access_token,
156  PUBSUB_API_HOST,
157  ),
158  cloud_project_id=cloud_project_id,
159  )
None __init__(self, ClientSession websession, str access_token, str host)
Definition: api.py:86
None __init__(self, ClientSession websession, config_entry_oauth2_flow.OAuth2Session oauth_session, str client_id, str client_secret)
Definition: api.py:40
GoogleNestSubscriber new_subscriber_with_token(HomeAssistant hass, str access_token, str project_id, str subscription_name)
Definition: api.py:133
AdminClient new_pubsub_admin_client(HomeAssistant hass, str access_token, str cloud_project_id)
Definition: api.py:150
GoogleNestSubscriber|None new_subscriber(HomeAssistant hass, ConfigEntry entry)
Definition: api.py:106