From de9253e7fa6e7f1ab155a325888eba727867856a Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:26:55 +0000 Subject: [PATCH 01/11] Add heos options flow --- homeassistant/components/heos/config_flow.py | 96 +++++++++++++++++++- homeassistant/components/heos/strings.json | 21 +++++ 2 files changed, 113 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 830d708effd84a..2e35b1f1502550 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,17 +1,27 @@ """Config flow to configure Heos.""" -from typing import TYPE_CHECKING, Any +import logging +from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse -from pyheos import Heos, HeosError, HeosOptions +from pyheos import CommandFailedError, Heos, HeosError, HeosOptions import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST +from homeassistant.config_entries import ( + ConfigEntry, + ConfigFlow, + ConfigFlowResult, + OptionsFlow, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers import selector from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + def format_title(host: str) -> str: """Format the title for config entries.""" @@ -36,6 +46,12 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Create the options flow.""" + return HeosOptionsFlowHandler() + async def async_step_ssdp( self, discovery_info: ssdp.SsdpServiceInfo ) -> ConfigFlowResult: @@ -100,3 +116,75 @@ async def async_step_reconfigure( data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), errors=errors, ) + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_USERNAME): selector.TextSelector(), + vol.Optional(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + } +) + + +class HeosOptionsFlowHandler(OptionsFlow): + """Define HEOS options flow.""" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Manage the options.""" + errors: dict[str, str] = {} + if user_input is not None: + authentication = CONF_USERNAME in user_input or CONF_PASSWORD in user_input + if authentication and CONF_USERNAME not in user_input: + errors["username"] = "username_missing" + if authentication and CONF_PASSWORD not in user_input: + errors["password"] = "password_missing" + + if not errors: + heos = cast( + Heos, self.config_entry.runtime_data.controller_manager.controller + ) + + try: + if authentication: + # Attempt to login + try: + await heos.sign_in( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + _LOGGER.info( + "Successfully signed-in to HEOS Account: %s", + heos.signed_in_username, + ) + except CommandFailedError as err: + log_level = logging.INFO + if err.error_id in (6, 8, 10): + errors["base"] = "invalid_auth" + else: + errors["base"] = "unknown" + log_level = logging.ERROR + + _LOGGER.log( + log_level, "Failed to sign-in to HEOS Account: %s", err + ) + else: + # Log out + await heos.sign_out() + _LOGGER.info("Successfully signed-out of HEOS Account") + except HeosError: + _LOGGER.exception("Unexpected error occurred during sign-in/out") + errors["base"] = "unknown" + + if not errors: + return self.async_create_entry(data=user_input) + + return self.async_show_form( + errors=errors, + step_id="init", + data_schema=self.add_suggested_values_to_schema( + OPTIONS_SCHEMA, user_input or self.config_entry.options + ), + ) diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index fe4fc63b449c34..cda20c2a814c57 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -31,6 +31,27 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "options": { + "step": { + "init": { + "description": "You can sign-in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign-out of your account.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "username": "The username or e-mail address of your HEOS Account.", + "password": "The password to your HEOS Account." + } + } + }, + "error": { + "username_missing": "Username is missing", + "password_missing": "Password is missing", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, "services": { "sign_in": { "name": "Sign in", From a37d50b9ad8ddc2134b1828421fb20c0017d47e9 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 16:19:16 +0000 Subject: [PATCH 02/11] Add options flow tests --- homeassistant/components/heos/config_flow.py | 14 +- tests/components/heos/conftest.py | 30 ++++- tests/components/heos/test_config_flow.py | 127 ++++++++++++++++++- 3 files changed, 159 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 2e35b1f1502550..673ed878974dad 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -139,9 +139,9 @@ async def async_step_init( if user_input is not None: authentication = CONF_USERNAME in user_input or CONF_PASSWORD in user_input if authentication and CONF_USERNAME not in user_input: - errors["username"] = "username_missing" + errors[CONF_USERNAME] = "username_missing" if authentication and CONF_PASSWORD not in user_input: - errors["password"] = "password_missing" + errors[CONF_PASSWORD] = "password_missing" if not errors: heos = cast( @@ -160,12 +160,12 @@ async def async_step_init( heos.signed_in_username, ) except CommandFailedError as err: - log_level = logging.INFO + errors["base"] = "unknown" + log_level = logging.ERROR + if err.error_id in (6, 8, 10): errors["base"] = "invalid_auth" - else: - errors["base"] = "unknown" - log_level = logging.ERROR + log_level = logging.INFO _LOGGER.log( log_level, "Failed to sign-in to HEOS Account: %s", err @@ -175,8 +175,8 @@ async def async_step_init( await heos.sign_out() _LOGGER.info("Successfully signed-out of HEOS Account") except HeosError: - _LOGGER.exception("Unexpected error occurred during sign-in/out") errors["base"] = "unknown" + _LOGGER.exception("Unexpected error occurred during sign-in/out") if not errors: return self.async_create_entry(data=user_input) diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 7b40ff5f749185..0e830190a9d6a6 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -18,21 +18,45 @@ import pytest_asyncio from homeassistant.components import ssdp -from homeassistant.components.heos import DOMAIN +from homeassistant.components.heos import ( + DOMAIN, + ControllerManager, + GroupManager, + HeosRuntimeData, + SourceManager, +) from homeassistant.const import CONF_HOST from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(): +def config_entry_fixture(heos_runtime_data): """Create a mock HEOS config entry.""" - return MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, title="HEOS System (via 127.0.0.1)", unique_id=DOMAIN, ) + entry.runtime_data = heos_runtime_data + return entry + + +@pytest.fixture(name="heos_runtime_data") +def heos_runtime_data_fixture(controller_manager, players): + """Create a mock HeosRuntimeData fixture.""" + return HeosRuntimeData( + controller_manager, Mock(GroupManager), Mock(SourceManager), players + ) + + +@pytest.fixture(name="controller_manager") +def controller_manager_fixture(controller): + """Create a mock controller manager fixture.""" + mock_controller_manager = Mock(ControllerManager) + mock_controller_manager.controller = controller + return mock_controller_manager @pytest.fixture(name="controller") diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 38382a817940b4..a02ade08fb405f 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Heos config flow module.""" -from pyheos import HeosError +from pyheos import CommandFailedError, HeosError from homeassistant.components import heos, ssdp from homeassistant.components.heos.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -190,3 +190,126 @@ async def test_reconfigure_cannot_connect_recovers( assert config_entry.unique_id == DOMAIN assert result["reason"] == "reconfigure_successful" assert result["type"] is FlowResultType.ABORT + + +async def test_options_flow_signs_in( + hass: HomeAssistant, config_entry, controller +) -> None: + """Test options flow signs-in with entered credentials.""" + config_entry.add_to_hass(hass) + + # Start the options flow. Entry has not current options. + assert CONF_USERNAME not in config_entry.options + assert CONF_PASSWORD not in config_entry.options + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["step_id"] == "init" + assert result["errors"] == {} + assert result["type"] is FlowResultType.FORM + + # Enter invalid credentials shows error + user_input = {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} + errors = [ + CommandFailedError("sign_in", "Invalid credentials", 6), + CommandFailedError("sign_in", "User not logged in", 8), + CommandFailedError("sign_in", "user not found", 10), + ] + for error in errors: + controller.sign_in.reset_mock() + controller.sign_in.side_effect = error + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 1 + assert controller.sign_out.call_count == 0 + assert result["step_id"] == "init" + assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] is FlowResultType.FORM + + # Unknown error validating credentials shows error + controller.sign_in.reset_mock() + controller.sign_in.side_effect = HeosError() + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 1 + assert controller.sign_out.call_count == 0 + assert result["step_id"] == "init" + assert result["errors"] == {"base": "unknown"} + assert result["type"] is FlowResultType.FORM + + # Enter valid credentials signs-in and creates entry + controller.sign_in.reset_mock() + controller.sign_in.side_effect = None + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 1 + assert controller.sign_out.call_count == 0 + assert result["data"] == user_input + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_options_flow_signs_out( + hass: HomeAssistant, config_entry, controller +) -> None: + """Test options flow signs-out when credentials cleared.""" + config_entry.add_to_hass(hass) + + # Start the options flow. Entry has not current options. + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["step_id"] == "init" + assert result["errors"] == {} + assert result["type"] is FlowResultType.FORM + + # Clear credentials + user_input = {} + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 0 + assert controller.sign_out.call_count == 1 + assert result["data"] == user_input + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_options_flow_missing_one_param_recovers( + hass: HomeAssistant, config_entry, controller +) -> None: + """Test options flow signs-in after recovering from only username or password being entered.""" + config_entry.add_to_hass(hass) + + # Start the options flow. Entry has not current options. + assert CONF_USERNAME not in config_entry.options + assert CONF_PASSWORD not in config_entry.options + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["step_id"] == "init" + assert result["errors"] == {} + assert result["type"] is FlowResultType.FORM + + # Enter only username + user_input = {CONF_USERNAME: "user"} + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert result["step_id"] == "init" + assert result["errors"] == {CONF_PASSWORD: "password_missing"} + assert result["type"] is FlowResultType.FORM + + # Enter only password + user_input = {CONF_PASSWORD: "pass"} + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert result["step_id"] == "init" + assert result["errors"] == {CONF_USERNAME: "username_missing"} + assert result["type"] is FlowResultType.FORM + + # Enter valid credentials + user_input = {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 1 + assert controller.sign_out.call_count == 0 + assert result["data"] == user_input + assert result["type"] is FlowResultType.CREATE_ENTRY From d400ed5a7cc04467c2e1050348fd0e5de88c165a Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 16:22:37 +0000 Subject: [PATCH 03/11] Test error condition during options sign out --- tests/components/heos/test_config_flow.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index a02ade08fb405f..5fe4cb262340dc 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -261,8 +261,21 @@ async def test_options_flow_signs_out( assert result["errors"] == {} assert result["type"] is FlowResultType.FORM - # Clear credentials + # Fail to sign-out, show error user_input = {} + controller.sign_out.side_effect = HeosError() + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 0 + assert controller.sign_out.call_count == 1 + assert result["step_id"] == "init" + assert result["errors"] == {"base": "unknown"} + assert result["type"] is FlowResultType.FORM + + # Clear credentials + controller.sign_out.reset_mock() + controller.sign_out.side_effect = None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input ) From e01d4e24be5357974b317a0f0c30a3c21d0adcc8 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:18:49 +0000 Subject: [PATCH 04/11] Use credentials when setting up --- homeassistant/components/heos/__init__.py | 32 ++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a7acd53739f43c..f17f8d173b79cf 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -7,10 +7,23 @@ from datetime import timedelta import logging -from pyheos import Heos, HeosError, HeosOptions, HeosPlayer, const as heos_const +from pyheos import ( + Credentials, + Heos, + HeosError, + HeosOptions, + HeosPlayer, + const as heos_const, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, + Platform, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -56,9 +69,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) host = entry.data[CONF_HOST] + credentials: Credentials | None = None + if CONF_USERNAME in entry.options and CONF_PASSWORD in entry.options: + credentials = Credentials( + entry.options[CONF_USERNAME], entry.options[CONF_PASSWORD] + ) + # Setting all_progress_events=False ensures that we only receive a # media position update upon start of playback or when media changes - controller = Heos(HeosOptions(host, all_progress_events=False, auto_reconnect=True)) + controller = Heos( + HeosOptions( + host, + all_progress_events=False, + auto_reconnect=True, + credentials=credentials, + ) + ) try: await controller.connect() # Auto reconnect only operates if initial connection was successful. From 19cbd578c2d9a95dca3fbe10a806d4d0b2d144f6 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 17:59:43 +0000 Subject: [PATCH 05/11] Update warning instructions --- homeassistant/components/heos/__init__.py | 6 +----- tests/components/heos/test_init.py | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index f17f8d173b79cf..9ff3c667bc86a4 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -109,11 +109,7 @@ async def disconnect_controller(event): favorites = await controller.get_favorites() else: _LOGGER.warning( - ( - "%s is not logged in to a HEOS account and will be unable to" - " retrieve HEOS favorites: Use the 'heos.sign_in' service to" - " sign-in to a HEOS account" - ), + "HEOS System (via %s) is not logged in: enter credentials in the integration options to access favorites and streaming services", host, ) inputs = await controller.get_input_sources() diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 8d2e3b68a22d6b..2ab750fb15512f 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -85,8 +85,7 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( assert controller.get_input_sources.call_count == 1 controller.disconnect.assert_not_called() assert ( - "127.0.0.1 is not logged in to a HEOS account and will be unable to retrieve " - "HEOS favorites: Use the 'heos.sign_in' service to sign-in to a HEOS account" + "HEOS System (via 127.0.0.1) is not logged in: enter credentials in the integration options to access favorites and streaming services" in caplog.text ) From c83defa83420845974f0fc2bb6afa631145f9086 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 18:09:35 +0000 Subject: [PATCH 06/11] Simplify exception logic --- homeassistant/components/heos/config_flow.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 673ed878974dad..769c9f59fc08c5 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -160,16 +160,13 @@ async def async_step_init( heos.signed_in_username, ) except CommandFailedError as err: - errors["base"] = "unknown" - log_level = logging.ERROR - - if err.error_id in (6, 8, 10): + if err.error_id in (6, 8, 10): # Auth-specific errors errors["base"] = "invalid_auth" - log_level = logging.INFO - - _LOGGER.log( - log_level, "Failed to sign-in to HEOS Account: %s", err - ) + _LOGGER.info( + "Failed to sign-in to HEOS Account: %s", err + ) + else: + raise # Re-raise unexpected error else: # Log out await heos.sign_out() From 44be7500f8da63b169c10f14b00a0297ef31586e Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 19:40:58 +0000 Subject: [PATCH 07/11] Cover unknown command error condition --- tests/components/heos/test_config_flow.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 5fe4cb262340dc..d6e59dc5ff26f1 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -225,6 +225,18 @@ async def test_options_flow_signs_in( assert result["errors"] == {"base": "invalid_auth"} assert result["type"] is FlowResultType.FORM + # Unknown command error validating credentials shows error + controller.sign_in.reset_mock() + controller.sign_in.side_effect = CommandFailedError("sign_in", "System error", 12) + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + assert controller.sign_in.call_count == 1 + assert controller.sign_out.call_count == 0 + assert result["step_id"] == "init" + assert result["errors"] == {"base": "unknown"} + assert result["type"] is FlowResultType.FORM + # Unknown error validating credentials shows error controller.sign_in.reset_mock() controller.sign_in.side_effect = HeosError() From a20aaa9ea44449a4da95a7faaef5540904f49d80 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 20:02:28 +0000 Subject: [PATCH 08/11] Add test for options --- tests/components/heos/conftest.py | 23 ++++++++++++++++++---- tests/components/heos/test_init.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 0e830190a9d6a6..6451f5bc69e161 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -19,13 +19,14 @@ from homeassistant.components import ssdp from homeassistant.components.heos import ( + CONF_PASSWORD, DOMAIN, ControllerManager, GroupManager, HeosRuntimeData, SourceManager, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_USERNAME from tests.common import MockConfigEntry @@ -43,6 +44,20 @@ def config_entry_fixture(heos_runtime_data): return entry +@pytest.fixture(name="config_entry_options") +def config_entry_options_fixture(heos_runtime_data): + """Create a mock HEOS config entry with options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1"}, + title="HEOS System (via 127.0.0.1)", + options={CONF_USERNAME: "user", CONF_PASSWORD: "pass"}, + unique_id=DOMAIN, + ) + entry.runtime_data = heos_runtime_data + return entry + + @pytest.fixture(name="heos_runtime_data") def heos_runtime_data_fixture(controller_manager, players): """Create a mock HeosRuntimeData fixture.""" @@ -67,6 +82,7 @@ def controller_fixture( mock_heos = Mock(Heos) for player in players.values(): player.heos = mock_heos + mock_heos.return_value = mock_heos mock_heos.dispatcher = dispatcher mock_heos.get_players.return_value = players mock_heos.players = players @@ -79,11 +95,10 @@ def controller_fixture( mock_heos.connection_state = const.STATE_CONNECTED mock_heos.get_groups.return_value = group mock_heos.create_group.return_value = None - mock = Mock(return_value=mock_heos) with ( - patch("homeassistant.components.heos.Heos", new=mock), - patch("homeassistant.components.heos.config_flow.Heos", new=mock), + patch("homeassistant.components.heos.Heos", new=mock_heos), + patch("homeassistant.components.heos.config_flow.Heos", new=mock_heos), ): yield mock_heos diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 2ab750fb15512f..33af2752a05c1d 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,18 +1,23 @@ """Tests for the init module.""" import asyncio +from typing import cast from unittest.mock import Mock, patch from pyheos import CommandFailedError, HeosError, const import pytest from homeassistant.components.heos import ( + CONF_PASSWORD, ControllerManager, + HeosOptions, HeosRuntimeData, async_setup_entry, async_unload_entry, ) from homeassistant.components.heos.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component @@ -61,6 +66,32 @@ async def test_async_setup_entry_loads_platforms( controller.disconnect.assert_not_called() +async def test_async_setup_entry_with_options_loads_platforms( + hass: HomeAssistant, + config_entry_options, + config, + controller, + input_sources, + favorites, +) -> None: + """Test load connects to heos with options, retrieves players, and loads platforms.""" + config_entry_options.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + # Assert options passed and methods called + assert config_entry_options.state is ConfigEntryState.LOADED + options = cast(HeosOptions, controller.call_args[0][0]) + assert options.host == config_entry_options.data[CONF_HOST] + assert options.credentials.username == config_entry_options.options[CONF_USERNAME] + assert options.credentials.password == config_entry_options.options[CONF_PASSWORD] + assert controller.connect.call_count == 1 + assert controller.get_players.call_count == 1 + assert controller.get_favorites.call_count == 1 + assert controller.get_input_sources.call_count == 1 + controller.disconnect.assert_not_called() + + async def test_async_setup_entry_not_signed_in_loads_platforms( hass: HomeAssistant, config_entry, From 3447d2c4e9fd2c8a3dab32a43efa2553e9ddc69e Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 27 Dec 2024 20:12:58 +0000 Subject: [PATCH 09/11] Correct const import location --- tests/components/heos/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 33af2752a05c1d..d104f3b75de291 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -8,7 +8,6 @@ import pytest from homeassistant.components.heos import ( - CONF_PASSWORD, ControllerManager, HeosOptions, HeosRuntimeData, @@ -17,7 +16,7 @@ ) from homeassistant.components.heos.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component From 65bd0f2d905cd10b15c911840905b6504fd58ac5 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 28 Dec 2024 18:05:31 +0000 Subject: [PATCH 10/11] Review feedback --- homeassistant/components/heos/__init__.py | 7 +++---- homeassistant/components/heos/quality_scale.yaml | 5 +---- homeassistant/components/heos/strings.json | 1 + tests/components/heos/test_init.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 9ff3c667bc86a4..2bb9a22eb004ae 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HeosConfigEntry) -> bool host = entry.data[CONF_HOST] credentials: Credentials | None = None - if CONF_USERNAME in entry.options and CONF_PASSWORD in entry.options: + if entry.options: credentials = Credentials( entry.options[CONF_USERNAME], entry.options[CONF_PASSWORD] ) @@ -108,9 +108,8 @@ async def disconnect_controller(event): if controller.is_signed_in: favorites = await controller.get_favorites() else: - _LOGGER.warning( - "HEOS System (via %s) is not logged in: enter credentials in the integration options to access favorites and streaming services", - host, + _LOGGER.info( + "The HEOS System is not logged in: Enter credentials in the integration options to access favorites and streaming services" ) inputs = await controller.get_input_sources() except HeosError as error: diff --git a/homeassistant/components/heos/quality_scale.yaml b/homeassistant/components/heos/quality_scale.yaml index 39c25486e52b7a..7ffac95218ec27 100644 --- a/homeassistant/components/heos/quality_scale.yaml +++ b/homeassistant/components/heos/quality_scale.yaml @@ -33,10 +33,7 @@ rules: status: todo comment: Actions currently only log and instead should raise exceptions. config-entry-unloading: done - docs-configuration-parameters: - status: done - comment: | - The integration doesn't provide any additional configuration parameters. + docs-configuration-parameters: done docs-installation-parameters: done entity-unavailable: done integration-owner: done diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index cda20c2a814c57..d6e83bad55a894 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -34,6 +34,7 @@ "options": { "step": { "init": { + "title": "HEOS Options", "description": "You can sign-in to your HEOS Account to access favorites, streaming services, and other features. Clearing the credentials will sign-out of your account.", "data": { "username": "[%key:common::config_flow::data::username%]", diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index d104f3b75de291..31ddb80edb75e5 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -115,7 +115,7 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( assert controller.get_input_sources.call_count == 1 controller.disconnect.assert_not_called() assert ( - "HEOS System (via 127.0.0.1) is not logged in: enter credentials in the integration options to access favorites and streaming services" + "The HEOS System is not logged in: Enter credentials in the integration options to access favorites and streaming services" in caplog.text ) From 2bc927474cb5e6f0421a660637a51e6d2499b582 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sun, 29 Dec 2024 00:38:17 +0000 Subject: [PATCH 11/11] Update per feedback --- homeassistant/components/heos/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 769c9f59fc08c5..a6499c1d25886d 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -149,7 +149,7 @@ async def async_step_init( ) try: - if authentication: + if user_input: # Attempt to login try: await heos.sign_in(