From 20d1a0fc7745e44f1ca5521b6a558a48a9cb7189 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 29 May 2023 03:44:45 +0200 Subject: [PATCH] Add Options flow to YouTube (#93667) * Add Options flow to YouTube * Add strings for options flow * Add strings for options flow * Add strings for options flow --- .../components/youtube/config_flow.py | 80 ++++++++++++++++--- homeassistant/components/youtube/strings.json | 10 +++ tests/components/youtube/test_config_flow.py | 34 +++++++- 3 files changed, 112 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/youtube/config_flow.py b/homeassistant/components/youtube/config_flow.py index 3d7748e9b57..a2adebc84af 100644 --- a/homeassistant/components/youtube/config_flow.py +++ b/homeassistant/components/youtube/config_flow.py @@ -11,8 +11,9 @@ from googleapiclient.errors import HttpError from googleapiclient.http import HttpRequest import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, OptionsFlowWithConfigEntry from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.selector import ( @@ -24,6 +25,19 @@ from homeassistant.helpers.selector import ( from .const import CONF_CHANNELS, DEFAULT_ACCESS, DOMAIN, LOGGER +async def get_resource(hass: HomeAssistant, token: str) -> Resource: + """Get Youtube resource async.""" + + def _build_resource() -> Resource: + return build( + "youtube", + "v3", + credentials=Credentials(token), + ) + + return await hass.async_add_executor_job(_build_resource) + + class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN ): @@ -36,6 +50,14 @@ class OAuth2FlowHandler( reauth_entry: ConfigEntry | None = None + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> YouTubeOptionsFlowHandler: + """Get the options flow for this handler.""" + return YouTubeOptionsFlowHandler(config_entry) + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -69,7 +91,7 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an entry for the flow, or update existing entry.""" try: - service = await self._get_resource(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + service = await get_resource(self.hass, data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # pylint: disable=no-member own_channel_request: HttpRequest = service.channels().list( part="snippet", mine=True @@ -116,7 +138,9 @@ class OAuth2FlowHandler( data=self._data, options=user_input, ) - service = await self._get_resource(self._data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + service = await get_resource( + self.hass, self._data[CONF_TOKEN][CONF_ACCESS_TOKEN] + ) # pylint: disable=no-member subscription_request: HttpRequest = service.subscriptions().list( part="snippet", mine=True, maxResults=50 @@ -140,12 +164,46 @@ class OAuth2FlowHandler( ), ) - async def _get_resource(self, token: str) -> Resource: - def _build_resource() -> Resource: - return build( - "youtube", - "v3", - credentials=Credentials(token), - ) - return await self.hass.async_add_executor_job(_build_resource) +class YouTubeOptionsFlowHandler(OptionsFlowWithConfigEntry): + """YouTube Options flow handler.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Initialize form.""" + if user_input is not None: + return self.async_create_entry( + title=self.config_entry.title, + data=user_input, + ) + service = await get_resource( + self.hass, self.config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] + ) + # pylint: disable=no-member + subscription_request: HttpRequest = service.subscriptions().list( + part="snippet", mine=True, maxResults=50 + ) + response = await self.hass.async_add_executor_job(subscription_request.execute) + selectable_channels = [ + SelectOptionDict( + value=subscription["snippet"]["resourceId"]["channelId"], + label=subscription["snippet"]["title"], + ) + for subscription in response["items"] + ] + return self.async_show_form( + step_id="init", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_CHANNELS): SelectSelector( + SelectSelectorConfig( + options=selectable_channels, multiple=True + ) + ), + } + ), + self.options, + ), + ) diff --git a/homeassistant/components/youtube/strings.json b/homeassistant/components/youtube/strings.json index f9a2d3af0ae..24369ab26f9 100644 --- a/homeassistant/components/youtube/strings.json +++ b/homeassistant/components/youtube/strings.json @@ -19,6 +19,16 @@ } } }, + "options": { + "step": { + "init": { + "description": "Select the channels you want to add.", + "data": { + "channels": "YouTube channels" + } + } + } + }, "entity": { "sensor": { "latest_upload": { diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index debe25c174f..ed33947b593 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -12,7 +12,14 @@ from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from . import MockService -from .conftest import CLIENT_ID, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, SCOPES, TITLE +from .conftest import ( + CLIENT_ID, + GOOGLE_AUTH_URI, + GOOGLE_TOKEN_URI, + SCOPES, + TITLE, + ComponentSetup, +) from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -267,3 +274,28 @@ async def test_flow_exception( result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" + + +async def test_options_flow( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test the full options flow.""" + await setup_integration() + with patch( + "homeassistant.components.youtube.config_flow.build", return_value=MockService() + ): + entry = hass.config_entries.async_entries(DOMAIN)[0] + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == {CONF_CHANNELS: ["UC_x5XG1OV2P6uZZ5FSM9Ttw"]}