From a2b1dd8a5fd333af597eb9c9d651430dbabbe519 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 27 May 2024 10:43:49 +0200 Subject: [PATCH] Add config flow to Media Extractor (#115717) --- .../components/media_extractor/__init__.py | 46 +++++++++++++-- .../components/media_extractor/config_flow.py | 32 +++++++++++ .../components/media_extractor/manifest.json | 4 +- .../components/media_extractor/strings.json | 7 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 5 +- tests/components/media_extractor/conftest.py | 14 ++++- .../media_extractor/test_config_flow.py | 56 +++++++++++++++++++ tests/components/media_extractor/test_init.py | 1 + 9 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/media_extractor/config_flow.py create mode 100644 tests/components/media_extractor/test_config_flow.py diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 56b768c26a2..479cdf90aaf 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -16,8 +16,10 @@ from homeassistant.components.media_player import ( MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, ServiceCall, ServiceResponse, @@ -25,6 +27,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import ( @@ -55,16 +58,49 @@ CONFIG_SCHEMA = vol.Schema( ) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Media Extractor from a config entry.""" + + return True + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the media extractor service.""" - async def extract_media_url(call: ServiceCall) -> ServiceResponse: - """Extract media url.""" - youtube_dl = YoutubeDL( - {"quiet": True, "logger": _LOGGER, "format": call.data[ATTR_FORMAT_QUERY]} + if DOMAIN in config: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.11.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Media extractor", + }, ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + ) + + async def extract_media_url(call: ServiceCall) -> ServiceResponse: + """Extract media url.""" + def extract_info() -> dict[str, Any]: + youtube_dl = YoutubeDL( + { + "quiet": True, + "logger": _LOGGER, + "format": call.data[ATTR_FORMAT_QUERY], + } + ) return cast( dict[str, Any], youtube_dl.extract_info( @@ -93,7 +129,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def play_media(call: ServiceCall) -> None: """Get stream URL and send it to the play_media service.""" - MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() + MediaExtractor(hass, config.get(DOMAIN, {}), call.data).extract_and_send() default_format_query = config.get(DOMAIN, {}).get( CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY diff --git a/homeassistant/components/media_extractor/config_flow.py b/homeassistant/components/media_extractor/config_flow.py new file mode 100644 index 00000000000..4343d0551e0 --- /dev/null +++ b/homeassistant/components/media_extractor/config_flow.py @@ -0,0 +1,32 @@ +"""Config flow for Media Extractor integration.""" + +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult + +from .const import DOMAIN + + +class MediaExtractorConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Media Extractor.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + if user_input is not None: + return self.async_create_entry(title="Media extractor", data={}) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + async def async_step_import( + self, import_config: dict[str, Any] + ) -> ConfigFlowResult: + """Handle import.""" + return self.async_create_entry(title="Media extractor", data={}) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 940d1d7bb18..77cad361431 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,10 +2,12 @@ "domain": "media_extractor", "name": "Media Extractor", "codeowners": ["@joostlek"], + "config_flow": true, "dependencies": ["media_player"], "documentation": "https://www.home-assistant.io/integrations/media_extractor", "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.04.09"] + "requirements": ["yt-dlp==2024.04.09"], + "single_config_entry": true } diff --git a/homeassistant/components/media_extractor/strings.json b/homeassistant/components/media_extractor/strings.json index 1af84b5b8c8..4c3743b5c12 100644 --- a/homeassistant/components/media_extractor/strings.json +++ b/homeassistant/components/media_extractor/strings.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + } + }, "services": { "play_media": { "name": "Play media", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b421fbd13ad..567c00d63e7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -318,6 +318,7 @@ FLOWS = { "matter", "meater", "medcom_ble", + "media_extractor", "melcloud", "melnor", "met", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 42088eaea8d..881e001cf12 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3509,8 +3509,9 @@ "media_extractor": { "name": "Media Extractor", "integration_type": "hub", - "config_flow": false, - "iot_class": "calculated" + "config_flow": true, + "iot_class": "calculated", + "single_config_entry": true }, "mediaroom": { "name": "Mediaroom", diff --git a/tests/components/media_extractor/conftest.py b/tests/components/media_extractor/conftest.py index 4b7411340ae..5aca118e2ef 100644 --- a/tests/components/media_extractor/conftest.py +++ b/tests/components/media_extractor/conftest.py @@ -1,7 +1,8 @@ -"""The tests for Media Extractor integration.""" +"""Common fixtures for the Media Extractor tests.""" +from collections.abc import Generator from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -53,3 +54,12 @@ def empty_media_extractor_config() -> dict[str, Any]: def audio_media_extractor_config() -> dict[str, Any]: """Media extractor config for audio.""" return {DOMAIN: {"default_query": AUDIO_QUERY}} + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.media_extractor.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/media_extractor/test_config_flow.py b/tests/components/media_extractor/test_config_flow.py new file mode 100644 index 00000000000..bfee5ec4879 --- /dev/null +++ b/tests/components/media_extractor/test_config_flow.py @@ -0,0 +1,56 @@ +"""Tests for the Media extractor config flow.""" + +from homeassistant.components.media_extractor.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result.get("type") is FlowResultType.CREATE_ENTRY + assert result.get("title") == "Media extractor" + assert result.get("data") == {} + assert result.get("options") == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_single_instance_allowed(hass: HomeAssistant) -> None: + """Test we abort if already setup.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={} + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow(hass: HomeAssistant, mock_setup_entry) -> None: + """Test import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + + assert result.get("type") is FlowResultType.CREATE_ENTRY + assert result.get("title") == "Media extractor" + assert result.get("data") == {} + assert result.get("options") == {} + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/media_extractor/test_init.py b/tests/components/media_extractor/test_init.py index 388ea3be1fd..ee74eb4660b 100644 --- a/tests/components/media_extractor/test_init.py +++ b/tests/components/media_extractor/test_init.py @@ -36,6 +36,7 @@ async def test_play_media_service_is_registered(hass: HomeAssistant) -> None: assert hass.services.has_service(DOMAIN, SERVICE_PLAY_MEDIA) assert hass.services.has_service(DOMAIN, SERVICE_EXTRACT_MEDIA_URL) + assert len(hass.config_entries.async_entries(DOMAIN)) @pytest.mark.parametrize(