Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for YouTube integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 import voluptuous as vol
10 from youtubeaio.helper import first
11 from youtubeaio.types import AuthScope, ForbiddenError
12 from youtubeaio.youtube import YouTube
13 
14 from homeassistant.config_entries import (
15  SOURCE_REAUTH,
16  ConfigEntry,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
21 from homeassistant.core import callback
22 from homeassistant.helpers import config_entry_oauth2_flow
23 from homeassistant.helpers.aiohttp_client import async_get_clientsession
25  SelectOptionDict,
26  SelectSelector,
27  SelectSelectorConfig,
28 )
29 
30 from .const import (
31  CHANNEL_CREATION_HELP_URL,
32  CONF_CHANNELS,
33  DEFAULT_ACCESS,
34  DOMAIN,
35  LOGGER,
36 )
37 
38 
40  config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
41 ):
42  """Config flow to handle Google OAuth2 authentication."""
43 
44  _data: dict[str, Any] = {}
45  _title: str = ""
46 
47  DOMAIN = DOMAIN
48 
49  _youtube: YouTube | None = None
50 
51  @staticmethod
52  @callback
54  config_entry: ConfigEntry,
55  ) -> YouTubeOptionsFlowHandler:
56  """Get the options flow for this handler."""
58 
59  @property
60  def logger(self) -> logging.Logger:
61  """Return logger."""
62  return logging.getLogger(__name__)
63 
64  @property
65  def extra_authorize_data(self) -> dict[str, Any]:
66  """Extra data that needs to be appended to the authorize url."""
67  return {
68  "scope": " ".join(DEFAULT_ACCESS),
69  # Add params to ensure we get back a refresh token
70  "access_type": "offline",
71  "prompt": "consent",
72  }
73 
74  async def async_step_reauth(
75  self, entry_data: Mapping[str, Any]
76  ) -> ConfigFlowResult:
77  """Perform reauth upon an API authentication error."""
78  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
79 
81  self, user_input: dict[str, Any] | None = None
82  ) -> ConfigFlowResult:
83  """Confirm reauth dialog."""
84  if user_input is None:
85  return self.async_show_form(step_id="reauth_confirm")
86  return await self.async_step_user()
87 
88  async def get_resource(self, token: str) -> YouTube:
89  """Get Youtube resource async."""
90  if self._youtube_youtube is None:
91  self._youtube_youtube = YouTube(session=async_get_clientsession(self.hass))
92  await self._youtube_youtube.set_user_authentication(token, [AuthScope.READ_ONLY])
93  return self._youtube_youtube
94 
95  async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
96  """Create an entry for the flow, or update existing entry."""
97  try:
98  youtube = await self.get_resourceget_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
99  own_channel = await first(youtube.get_user_channels())
100  if own_channel is None or own_channel.snippet is None:
101  return self.async_abort(
102  reason="no_channel",
103  description_placeholders={"support_url": CHANNEL_CREATION_HELP_URL},
104  )
105  except ForbiddenError as ex:
106  error = ex.args[0]
107  return self.async_abort(
108  reason="access_not_configured",
109  description_placeholders={"message": error},
110  )
111  except Exception as ex: # noqa: BLE001
112  LOGGER.error("Unknown error occurred: %s", ex.args)
113  return self.async_abort(reason="unknown")
114  self._title_title = own_channel.snippet.title
115  self._data_data = data
116 
117  await self.async_set_unique_id(own_channel.channel_id)
118  if self.source != SOURCE_REAUTH:
119  self._abort_if_unique_id_configured()
120 
121  return await self.async_step_channelsasync_step_channels()
122 
123  self._abort_if_unique_id_mismatch(
124  reason="wrong_account",
125  description_placeholders={"title": self._title_title},
126  )
127 
128  return self.async_update_reload_and_abort(self._get_reauth_entry(), data=data)
129 
131  self, user_input: dict[str, Any] | None = None
132  ) -> ConfigFlowResult:
133  """Select which channels to track."""
134  if user_input:
135  return self.async_create_entry(
136  title=self._title_title,
137  data=self._data_data,
138  options=user_input,
139  )
140  youtube = await self.get_resourceget_resource(self._data_data[CONF_TOKEN][CONF_ACCESS_TOKEN])
141  selectable_channels = [
143  value=subscription.snippet.channel_id,
144  label=subscription.snippet.title,
145  )
146  async for subscription in youtube.get_user_subscriptions()
147  ]
148  if not selectable_channels:
149  return self.async_abort(reason="no_subscriptions")
150  return self.async_show_form(
151  step_id="channels",
152  data_schema=vol.Schema(
153  {
154  vol.Required(CONF_CHANNELS): SelectSelector(
155  SelectSelectorConfig(options=selectable_channels, multiple=True)
156  ),
157  }
158  ),
159  )
160 
161 
163  """YouTube Options flow handler."""
164 
165  async def async_step_init(
166  self, user_input: dict[str, Any] | None = None
167  ) -> ConfigFlowResult:
168  """Initialize form."""
169  if user_input is not None:
170  return self.async_create_entryasync_create_entry(
171  title=self.config_entryconfig_entryconfig_entry.title,
172  data=user_input,
173  )
174  youtube = YouTube(session=async_get_clientsession(self.hass))
175  await youtube.set_user_authentication(
176  self.config_entryconfig_entryconfig_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN], [AuthScope.READ_ONLY]
177  )
178  selectable_channels = [
180  value=subscription.snippet.channel_id,
181  label=subscription.snippet.title,
182  )
183  async for subscription in youtube.get_user_subscriptions()
184  ]
185  return self.async_show_formasync_show_form(
186  step_id="init",
187  data_schema=self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
188  vol.Schema(
189  {
190  vol.Required(CONF_CHANNELS): SelectSelector(
192  options=selectable_channels, multiple=True
193  )
194  ),
195  }
196  ),
197  self.config_entryconfig_entryconfig_entry.options,
198  ),
199  )
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:76
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:82
ConfigFlowResult async_step_channels(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:132
ConfigFlowResult async_oauth_create_entry(self, dict[str, Any] data)
Definition: config_flow.py:95
YouTubeOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:55
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:167
None config_entry(self, ConfigEntry value)
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)