Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for GitHub integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import TYPE_CHECKING, Any
7 
8 from aiogithubapi import (
9  GitHubAPI,
10  GitHubDeviceAPI,
11  GitHubException,
12  GitHubLoginDeviceModel,
13  GitHubLoginOauthModel,
14 )
15 from aiogithubapi.const import OAUTH_USER_LOGIN
16 import voluptuous as vol
17 
18 from homeassistant.config_entries import (
19  ConfigEntry,
20  ConfigFlow,
21  ConfigFlowResult,
22  OptionsFlow,
23 )
24 from homeassistant.const import CONF_ACCESS_TOKEN
25 from homeassistant.core import HomeAssistant, callback
27  SERVER_SOFTWARE,
28  async_get_clientsession,
29 )
31 
32 from .const import CLIENT_ID, CONF_REPOSITORIES, DEFAULT_REPOSITORIES, DOMAIN, LOGGER
33 
34 
35 async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]:
36  """Return a list of repositories that the user owns or has starred."""
37  client = GitHubAPI(token=access_token, session=async_get_clientsession(hass))
38  repositories = set()
39 
40  async def _get_starred_repositories() -> None:
41  response = await client.user.starred(params={"per_page": 100})
42  if not response.is_last_page:
43  results = await asyncio.gather(
44  *(
45  client.user.starred(
46  params={"per_page": 100, "page": page_number},
47  )
48  for page_number in range(
49  response.next_page_number, response.last_page_number + 1
50  )
51  )
52  )
53  for result in results:
54  response.data.extend(result.data)
55 
56  repositories.update(response.data)
57 
58  async def _get_personal_repositories() -> None:
59  response = await client.user.repos(params={"per_page": 100})
60  if not response.is_last_page:
61  results = await asyncio.gather(
62  *(
63  client.user.repos(
64  params={"per_page": 100, "page": page_number},
65  )
66  for page_number in range(
67  response.next_page_number, response.last_page_number + 1
68  )
69  )
70  )
71  for result in results:
72  response.data.extend(result.data)
73 
74  repositories.update(response.data)
75 
76  try:
77  await asyncio.gather(
78  *(
79  _get_starred_repositories(),
80  _get_personal_repositories(),
81  )
82  )
83 
84  except GitHubException:
85  return DEFAULT_REPOSITORIES
86 
87  if len(repositories) == 0:
88  return DEFAULT_REPOSITORIES
89 
90  return sorted(
91  (repo.full_name for repo in repositories),
92  key=str.casefold,
93  )
94 
95 
96 class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
97  """Handle a config flow for GitHub."""
98 
99  VERSION = 1
100 
101  login_task: asyncio.Task | None = None
102 
103  def __init__(self) -> None:
104  """Initialize."""
105  self._device_device: GitHubDeviceAPI | None = None
106  self._login_login: GitHubLoginOauthModel | None = None
107  self._login_device_login_device: GitHubLoginDeviceModel | None = None
108 
109  async def async_step_user(
110  self,
111  user_input: dict[str, Any] | None = None,
112  ) -> ConfigFlowResult:
113  """Handle the initial step."""
114  if self._async_current_entries_async_current_entries():
115  return self.async_abortasync_abortasync_abort(reason="already_configured")
116 
117  return await self.async_step_deviceasync_step_device(user_input)
118 
119  async def async_step_device(
120  self,
121  user_input: dict[str, Any] | None = None,
122  ) -> ConfigFlowResult:
123  """Handle device steps."""
124 
125  async def _wait_for_login() -> None:
126  if TYPE_CHECKING:
127  # mypy is not aware that we can't get here without having these set already
128  assert self._device_device is not None
129  assert self._login_device_login_device is not None
130 
131  response = await self._device_device.activation(
132  device_code=self._login_device_login_device.device_code
133  )
134  self._login_login = response.data
135 
136  if not self._device_device:
137  self._device_device = GitHubDeviceAPI(
138  client_id=CLIENT_ID,
139  session=async_get_clientsession(self.hass),
140  client_name=SERVER_SOFTWARE,
141  )
142 
143  try:
144  response = await self._device_device.register()
145  self._login_device_login_device = response.data
146  except GitHubException as exception:
147  LOGGER.exception(exception)
148  return self.async_abortasync_abortasync_abort(reason="could_not_register")
149 
150  if self.login_tasklogin_task is None:
151  self.login_tasklogin_task = self.hass.async_create_task(_wait_for_login())
152 
153  if self.login_tasklogin_task.done():
154  if self.login_tasklogin_task.exception():
155  return self.async_show_progress_doneasync_show_progress_done(next_step_id="could_not_register")
156  return self.async_show_progress_doneasync_show_progress_done(next_step_id="repositories")
157 
158  if TYPE_CHECKING:
159  # mypy is not aware that we can't get here without having this set already
160  assert self._login_device_login_device is not None
161 
162  return self.async_show_progressasync_show_progress(
163  step_id="device",
164  progress_action="wait_for_device",
165  description_placeholders={
166  "url": OAUTH_USER_LOGIN,
167  "code": self._login_device_login_device.user_code,
168  },
169  progress_task=self.login_tasklogin_task,
170  )
171 
173  self,
174  user_input: dict[str, Any] | None = None,
175  ) -> ConfigFlowResult:
176  """Handle repositories step."""
177 
178  if TYPE_CHECKING:
179  # mypy is not aware that we can't get here without having this set already
180  assert self._login_login is not None
181 
182  if not user_input:
183  repositories = await get_repositories(self.hass, self._login_login.access_token)
184  return self.async_show_formasync_show_formasync_show_form(
185  step_id="repositories",
186  data_schema=vol.Schema(
187  {
188  vol.Required(CONF_REPOSITORIES): cv.multi_select(
189  {k: k for k in repositories}
190  ),
191  }
192  ),
193  )
194 
195  return self.async_create_entryasync_create_entryasync_create_entry(
196  title="",
197  data={CONF_ACCESS_TOKEN: self._login_login.access_token},
198  options={CONF_REPOSITORIES: user_input[CONF_REPOSITORIES]},
199  )
200 
202  self,
203  user_input: dict[str, Any] | None = None,
204  ) -> ConfigFlowResult:
205  """Handle issues that need transition await from progress step."""
206  return self.async_abortasync_abortasync_abort(reason="could_not_register")
207 
208  @staticmethod
209  @callback
211  config_entry: ConfigEntry,
212  ) -> OptionsFlowHandler:
213  """Get the options flow for this handler."""
214  return OptionsFlowHandler()
215 
216 
218  """Handle a option flow for GitHub."""
219 
220  async def async_step_init(
221  self,
222  user_input: dict[str, Any] | None = None,
223  ) -> ConfigFlowResult:
224  """Handle options flow."""
225  if not user_input:
226  configured_repositories: list[str] = self.config_entryconfig_entryconfig_entry.options[
227  CONF_REPOSITORIES
228  ]
229  repositories = await get_repositories(
230  self.hass, self.config_entryconfig_entryconfig_entry.data[CONF_ACCESS_TOKEN]
231  )
232 
233  # In case the user has removed a starred repository that is already tracked
234  for repository in configured_repositories:
235  if repository not in repositories:
236  repositories.append(repository)
237 
238  return self.async_show_formasync_show_form(
239  step_id="init",
240  data_schema=vol.Schema(
241  {
242  vol.Required(
243  CONF_REPOSITORIES,
244  default=configured_repositories,
245  ): cv.multi_select({k: k for k in repositories}),
246  }
247  ),
248  )
249 
250  return self.async_create_entryasync_create_entry(title="", data=user_input)
ConfigFlowResult async_step_could_not_register(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:204
ConfigFlowResult async_step_repositories(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:175
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:212
ConfigFlowResult async_step_device(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:122
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:112
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:223
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult 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)
None config_entry(self, ConfigEntry value)
_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_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
list[str] get_repositories(HomeAssistant hass, str access_token)
Definition: config_flow.py:35
def register(HomeAssistant hass, Heos controller)
Definition: services.py:29
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)