diff --git a/.coveragerc b/.coveragerc index 1ccb9e461df38..af5f2e598b488 100644 --- a/.coveragerc +++ b/.coveragerc @@ -139,7 +139,8 @@ omit = homeassistant/components/blinksticklight/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* - homeassistant/components/bluesound/* + homeassistant/components/bluesound/__init__.py + homeassistant/components/bluesound/media_player.py homeassistant/components/bluetooth_tracker/* homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f954675f4d4bc..03a9200e30902 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -191,6 +191,7 @@ build.json @home-assistant/supervisor /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn +/tests/components/bluesound/ @thrawnarn /homeassistant/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco /homeassistant/components/bluetooth_adapters/ @bdraco diff --git a/homeassistant/components/bluesound/__init__.py b/homeassistant/components/bluesound/__init__.py index 9dbe0f754fb04..9ecc7d60f42e0 100644 --- a/homeassistant/components/bluesound/__init__.py +++ b/homeassistant/components/bluesound/__init__.py @@ -1 +1,28 @@ """The bluesound component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN +from .media_player import setup_services + +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the Bluesound entry.""" + await hass.config_entries.async_forward_entry_setup( + config_entry, Platform.MEDIA_PLAYER + ) + + return True + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Bluesound.""" + setup_services(hass) + + return True diff --git a/homeassistant/components/bluesound/config_flow.py b/homeassistant/components/bluesound/config_flow.py new file mode 100644 index 0000000000000..80ab80863631a --- /dev/null +++ b/homeassistant/components/bluesound/config_flow.py @@ -0,0 +1,42 @@ +"""Config flow for bluesound.""" + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class BluesoundConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Bluesound config flow.""" + + VERSION = 1 + MINOR_VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initiated by the user.""" + if user_input is not None: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default="Bluesound"): str, + vol.Required(CONF_HOST, description="host"): str, + vol.Optional(CONF_PORT, default=11000): int, + } + ), + ) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index efd7dd0f347bc..abb8e561dfcdc 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -2,6 +2,7 @@ "domain": "bluesound", "name": "Bluesound", "codeowners": ["@thrawnarn"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bluesound", "iot_class": "local_polling", "requirements": ["xmltodict==0.13.0"] diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 6c63067a1c19d..ce02eae13ff95 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -18,7 +18,6 @@ from homeassistant.components import media_source from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -26,11 +25,10 @@ MediaType, async_process_play_media_url, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, - CONF_HOSTS, - CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -41,7 +39,6 @@ from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -70,21 +67,6 @@ UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOSTS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } - ], - ) - } -) - BS_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) BS_JOIN_SCHEMA = BS_SCHEMA.extend({vol.Required(ATTR_MASTER): cv.entity_id}) @@ -141,34 +123,25 @@ def _add_player_cb(): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Bluesound platforms.""" + """Set up the Bluesound entry.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] - if discovery_info: - _add_player( - hass, - async_add_entities, - discovery_info.get(CONF_HOST), - discovery_info.get(CONF_PORT), - ) - return - - if hosts := config.get(CONF_HOSTS): - for host in hosts: - _add_player( - hass, - async_add_entities, - host.get(CONF_HOST), - host.get(CONF_PORT), - host.get(CONF_NAME), - ) + _add_player( + hass, + async_add_entities, + config_entry.data.get(CONF_HOST), + config_entry.data.get(CONF_PORT), + ) + + +def setup_services(hass: HomeAssistant): + """Set up services for Bluesound component.""" async def async_service_handler(service: ServiceCall) -> None: """Map services to method of Bluesound devices.""" diff --git a/homeassistant/components/bluesound/strings.json b/homeassistant/components/bluesound/strings.json index f41c34a7449ac..a55fc4bdf492c 100644 --- a/homeassistant/components/bluesound/strings.json +++ b/homeassistant/components/bluesound/strings.json @@ -1,4 +1,15 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "name": "[%key:common::config_flow::data::name%]" + } + } + } + }, "services": { "join": { "name": "Join", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6f6ce2379049c..02b9cf48afd36 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -75,6 +75,7 @@ "blink", "blue_current", "bluemaestro", + "bluesound", "bluetooth", "bmw_connected_drive", "bond", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cf5f352f22c5d..6ed6351a66d0c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -708,7 +708,7 @@ "bluesound": { "name": "Bluesound", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "bluetooth": { diff --git a/tests/components/bluesound/__init__.py b/tests/components/bluesound/__init__.py new file mode 100644 index 0000000000000..f8a3701422ec8 --- /dev/null +++ b/tests/components/bluesound/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluesound integration.""" diff --git a/tests/components/bluesound/conftest.py b/tests/components/bluesound/conftest.py new file mode 100644 index 0000000000000..ec9b747a6257c --- /dev/null +++ b/tests/components/bluesound/conftest.py @@ -0,0 +1,15 @@ +"""Common fixtures for the Bluesound tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.bluesound.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/bluesound/test_config_flow.py b/tests/components/bluesound/test_config_flow.py new file mode 100644 index 0000000000000..dd3f57ba0bdce --- /dev/null +++ b/tests/components/bluesound/test_config_flow.py @@ -0,0 +1,36 @@ +"""Test the Bluesound config flow.""" + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.bluesound.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_NAME: "test-name", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 11000, + CONF_NAME: "test-name", + } + assert len(mock_setup_entry.mock_calls) == 1