diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 919f9b9be89..36f8fbec59d 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -14,8 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_REGIONS_MAP, DOMAIN -from .util import get_brand_for_region +from .const import CONF_BRAND, CONF_BRANDS_MAP, CONF_REGIONS_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,8 +27,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")] - brand = get_brand_for_region(region) + brand = CONF_BRANDS_MAP[entry.data.get(CONF_BRAND, "Whirlpool")] backend_selector = BackendSelector(brand, region) + auth = Auth( backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session ) diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index e8ab765feba..5e1cb102d77 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -18,8 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_REGIONS_MAP, DOMAIN -from .util import get_brand_for_region +from .const import CONF_BRAND, CONF_BRANDS_MAP, CONF_REGIONS_MAP, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -29,10 +28,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Required(CONF_REGION): vol.In(list(CONF_REGIONS_MAP)), + vol.Required(CONF_BRAND): vol.In(list(CONF_BRANDS_MAP)), } ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_BRAND): vol.In(list(CONF_BRANDS_MAP)), + } +) async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: @@ -42,7 +47,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, """ session = async_get_clientsession(hass) region = CONF_REGIONS_MAP[data[CONF_REGION]] - brand = get_brand_for_region(region) + brand = CONF_BRANDS_MAP[data[CONF_BRAND]] backend_selector = BackendSelector(brand, region) auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD], session) try: @@ -55,7 +60,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, appliances_manager = AppliancesManager(backend_selector, auth, session) await appliances_manager.fetch_appliances() - if appliances_manager.aircons is None and appliances_manager.washer_dryers is None: + + if not appliances_manager.aircons and not appliances_manager.washer_dryers: raise NoAppliances return {"title": data[CONF_USERNAME]} @@ -84,10 +90,8 @@ class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN): if user_input: assert self.entry is not None password = user_input[CONF_PASSWORD] - data = { - **self.entry.data, - CONF_PASSWORD: password, - } + brand = user_input[CONF_BRAND] + data = {**self.entry.data, CONF_PASSWORD: password, CONF_BRAND: brand} try: await validate_input(self.hass, data) @@ -96,13 +100,7 @@ class WhirlpoolConfigFlow(ConfigFlow, domain=DOMAIN): except (CannotConnect, TimeoutError): errors["base"] = "cannot_connect" else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_PASSWORD: password, - }, - ) + self.hass.config_entries.async_update_entry(self.entry, data=data) await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index b472c7f9156..63a58f54c1d 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,10 +1,17 @@ """Constants for the Whirlpool Appliances integration.""" -from whirlpool.backendselector import Region +from whirlpool.backendselector import Brand, Region DOMAIN = "whirlpool" +CONF_BRAND = "brand" CONF_REGIONS_MAP = { "EU": Region.EU, "US": Region.US, } + +CONF_BRANDS_MAP = { + "Whirlpool": Brand.Whirlpool, + "Maytag": Brand.Maytag, + "KitchenAid": Brand.KitchenAid, +} diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 4c3ce680323..0c46580ceeb 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.4"] + "requirements": ["whirlpool-sixth-sense==0.18.6"] } diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index d49459cc726..8c74f01298e 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -206,7 +206,7 @@ class WasherDryerClass(SensorEntity): self._wd.register_attr_callback(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: - """Close Whrilpool Appliance sockets before removing.""" + """Close Whirlpool Appliance sockets before removing.""" self._wd.unregister_attr_callback(self.async_write_ha_state) @property diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index a24e42304d0..b1658947263 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -2,9 +2,27 @@ "config": { "step": { "user": { + "title": "Configure your Whirlpool account", "data": { "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "[%key:common::config_flow::data::password%]", + "region": "Region", + "brand": "Brand" + }, + "data_description": { + "brand": "Please choose the brand of the mobile app you use, or the brand of the appliances in your account" + } + }, + "reauth_confirm": { + "title": "Correct your Whirlpool account credentials", + "description": "For 'brand', please choose the brand of the mobile app you use, or the brand of the appliances in your account", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "region": "Region", + "brand": "Brand" + }, + "data_description": { + "brand": "Please choose the brand of the mobile app you use, or the brand of the appliances in your account" } } }, diff --git a/homeassistant/components/whirlpool/util.py b/homeassistant/components/whirlpool/util.py deleted file mode 100644 index 55b094f76ac..00000000000 --- a/homeassistant/components/whirlpool/util.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Utility functions for the Whirlpool Sixth Sense integration.""" - -from whirlpool.backendselector import Brand, Region - - -def get_brand_for_region(region: Region) -> Brand: - """Get the correct brand for each region.""" - return Brand.Maytag if region == Region.US else Brand.Whirlpool diff --git a/requirements_all.txt b/requirements_all.txt index 12197aca771..b10ec3b082d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2841,7 +2841,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.4 +whirlpool-sixth-sense==0.18.6 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd1fa81f91f..b9c44739a32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2188,7 +2188,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.4 +whirlpool-sixth-sense==0.18.6 # homeassistant.components.whois whois==0.9.27 diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index eda7541ca56..ac52031a0fc 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -1,13 +1,14 @@ """Tests for the Whirlpool Sixth Sense integration.""" - -from homeassistant.components.whirlpool.const import DOMAIN +from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, region: str = "EU", brand: str = "Whirlpool" +) -> MockConfigEntry: """Set up the Whirlpool integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -15,6 +16,7 @@ async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfi CONF_USERNAME: "nobody", CONF_PASSWORD: "qwerty", CONF_REGION: region, + CONF_BRAND: brand, }, ) diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 7af14f9991a..e386012265c 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -16,13 +16,26 @@ MOCK_SAID4 = "said4" @pytest.fixture( name="region", - params=[("EU", Region.EU, Brand.Whirlpool), ("US", Region.US, Brand.Maytag)], + params=[("EU", Region.EU), ("US", Region.US)], ) def fixture_region(request): """Return a region for input.""" return request.param +@pytest.fixture( + name="brand", + params=[ + ("Whirlpool", Brand.Whirlpool), + ("KitchenAid", Brand.KitchenAid), + ("Maytag", Brand.Maytag), + ], +) +def fixture_brand(request): + """Return a brand for input.""" + return request.param + + @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): """Set up Auth fixture.""" diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 5d2375d2668..d164bff6a61 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -6,7 +6,7 @@ import aiohttp from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries -from homeassistant.components.whirlpool.const import DOMAIN +from homeassistant.components.whirlpool.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,10 +19,7 @@ CONFIG_INPUT = { } -async def test_form( - hass: HomeAssistant, - region, -) -> None: +async def test_form(hass: HomeAssistant, region, brand) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -48,7 +45,7 @@ async def test_form( ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) await hass.async_block_till_done() @@ -58,12 +55,13 @@ async def test_form( "username": "test-username", "password": "test-password", "region": region[0], + "brand": brand[0], } assert len(mock_setup_entry.mock_calls) == 1 - mock_backend_selector.assert_called_once_with(region[2], region[1]) + mock_backend_selector.assert_called_once_with(brand[1], region[1]) -async def test_form_invalid_auth(hass: HomeAssistant, region) -> None: +async def test_form_invalid_auth(hass: HomeAssistant, region, brand) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -74,16 +72,13 @@ async def test_form_invalid_auth(hass: HomeAssistant, region) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT - | { - "region": region[0], - }, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -97,13 +92,14 @@ async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: +async def test_form_auth_timeout(hass: HomeAssistant, region, brand) -> None: """Test we handle auth timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -117,13 +113,14 @@ async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: +async def test_form_generic_auth_exception(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -137,17 +134,18 @@ async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(hass: HomeAssistant, region) -> None: +async def test_form_already_configured(hass: HomeAssistant, region, brand) -> None: """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -174,6 +172,7 @@ async def test_form_already_configured(hass: HomeAssistant, region) -> None: CONFIG_INPUT | { "region": region[0], + "brand": brand[0], }, ) await hass.async_block_till_done() @@ -182,8 +181,8 @@ async def test_form_already_configured(hass: HomeAssistant, region) -> None: assert result2["reason"] == "already_configured" -async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: - """Test we get and error with no appliances.""" +async def test_no_appliances_flow(hass: HomeAssistant, region, brand) -> None: + """Test we get an error with no appliances.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -200,7 +199,7 @@ async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - CONFIG_INPUT | {"region": region[0]}, + CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) await hass.async_block_till_done() @@ -208,11 +207,11 @@ async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: assert result2["errors"] == {"base": "no_appliances"} -async def test_reauth_flow(hass: HomeAssistant, region) -> None: +async def test_reauth_flow(hass: HomeAssistant, region, brand) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -224,7 +223,7 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result["step_id"] == "reauth_confirm" @@ -246,7 +245,7 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() @@ -256,15 +255,16 @@ async def test_reauth_flow(hass: HomeAssistant, region) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", "region": region[0], + "brand": brand[0], } -async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: +async def test_reauth_flow_auth_error(hass: HomeAssistant, region, brand) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -280,6 +280,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", "region": region[0], + "brand": brand[0], }, ) @@ -295,7 +296,7 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() @@ -303,12 +304,14 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> None: +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, region, brand +) -> None: """Test a connection error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -320,7 +323,7 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> Non "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data=CONFIG_INPUT | {"region": region[0]}, + data=CONFIG_INPUT | {"region": region[0], "brand": brand[0]}, ) assert result["step_id"] == "reauth_confirm" @@ -339,7 +342,7 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> Non ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_PASSWORD: "new-password"}, + {CONF_PASSWORD: "new-password", CONF_BRAND: brand[0]}, ) await hass.async_block_till_done() diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index b5c8cf3d481..f9d28e78a06 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -7,7 +7,7 @@ from whirlpool.backendselector import Brand, Region from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from . import init_integration, init_integration_with_entry @@ -19,13 +19,14 @@ async def test_setup( hass: HomeAssistant, mock_backend_selector_api: MagicMock, region, + brand, mock_aircon_api_instances: MagicMock, ) -> None: """Test setup.""" - entry = await init_integration(hass, region[0]) + entry = await init_integration(hass, region[0], brand[0]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED - mock_backend_selector_api.assert_called_once_with(region[2], region[1]) + mock_backend_selector_api.assert_called_once_with(brand[1], region[1]) async def test_setup_region_fallback( @@ -51,6 +52,31 @@ async def test_setup_region_fallback( mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, Region.EU) +async def test_setup_brand_fallback( + hass: HomeAssistant, + region, + mock_backend_selector_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test setup when no brand is available on the ConfigEntry. + + This can happen after a version update, since the brand was not selected or stored in the earlier versions. + """ + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + CONF_REGION: region[0], + }, + ) + entry = await init_integration_with_entry(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, region[1]) + + async def test_setup_http_exception( hass: HomeAssistant, mock_auth_api: MagicMock,