Add support for US in the Whirlpool integration (#77237)

* Support US region in the Whirlpool integration

* Force maytag brand for US region

* Add missing util.py file

* Fix import after merge

* run black

* Missing region key in config flow test

* Fixed Generic config entry

* fixed typos in dict

* Remove redundant list const

Co-authored-by: mkmer <mike.j.kasper@gmail.com>
This commit is contained in:
Abílio Costa 2022-12-30 08:13:47 +00:00 committed by GitHub
parent 7aadcc1f97
commit 0e8164c07a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 58 deletions

View File

@ -5,14 +5,15 @@ import logging
import aiohttp import aiohttp
from whirlpool.appliancesmanager import AppliancesManager from whirlpool.appliancesmanager import AppliancesManager
from whirlpool.auth import Auth from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector, Brand, Region from whirlpool.backendselector import BackendSelector
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN from .const import CONF_REGIONS_MAP, DOMAIN
from .util import get_brand_for_region
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,8 +24,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Whirlpool Sixth Sense from a config entry.""" """Set up Whirlpool Sixth Sense from a config entry."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")]
auth = Auth(backend_selector, entry.data["username"], entry.data["password"]) brand = get_brand_for_region(region)
backend_selector = BackendSelector(brand, region)
auth = Auth(backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
try: try:
await auth.do_auth(store=False) await auth.do_auth(store=False)
except aiohttp.ClientError as ex: except aiohttp.ClientError as ex:

View File

@ -9,18 +9,24 @@ from typing import Any
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from whirlpool.auth import Auth from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector, Brand, Region from whirlpool.backendselector import BackendSelector
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN from .const import CONF_REGIONS_MAP, DOMAIN
from .util import get_brand_for_region
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema( STEP_USER_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION): vol.In(list(CONF_REGIONS_MAP)),
}
) )
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
@ -33,7 +39,9 @@ async def validate_input(
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
""" """
backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) region = CONF_REGIONS_MAP[data[CONF_REGION]]
brand = get_brand_for_region(region)
backend_selector = BackendSelector(brand, region)
auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD])
try: try:
await auth.do_auth() await auth.do_auth()
@ -68,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.entry is not None assert self.entry is not None
password = user_input[CONF_PASSWORD] password = user_input[CONF_PASSWORD]
data = { data = {
CONF_USERNAME: self.entry.data[CONF_USERNAME], **self.entry.data,
CONF_PASSWORD: password, CONF_PASSWORD: password,
} }

View File

@ -1,3 +1,10 @@
"""Constants for the Whirlpool Sixth Sense integration.""" """Constants for the Whirlpool Sixth Sense integration."""
from whirlpool.backendselector import Region
DOMAIN = "whirlpool" DOMAIN = "whirlpool"
CONF_REGIONS_MAP = {
"EU": Region.EU,
"US": Region.US,
}

View File

@ -3,7 +3,7 @@
"name": "Whirlpool Sixth Sense", "name": "Whirlpool Sixth Sense",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/whirlpool", "documentation": "https://www.home-assistant.io/integrations/whirlpool",
"requirements": ["whirlpool-sixth-sense==0.17.0"], "requirements": ["whirlpool-sixth-sense==0.17.1"],
"codeowners": ["@abmantis"], "codeowners": ["@abmantis"],
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["whirlpool"] "loggers": ["whirlpool"]

View File

@ -0,0 +1,8 @@
"""Utility functions for the Whirlpool Sixth Sense integration."""
from whirlpool.backendselector import Brand, Region
def get_brand_for_region(region: Region) -> bool:
"""Get the correct brand for each region."""
return Brand.Maytag if region == Region.US else Brand.Whirlpool

View File

@ -2567,7 +2567,7 @@ waterfurnace==1.1.0
webexteamssdk==1.1.1 webexteamssdk==1.1.1
# homeassistant.components.whirlpool # homeassistant.components.whirlpool
whirlpool-sixth-sense==0.17.0 whirlpool-sixth-sense==0.17.1
# homeassistant.components.whois # homeassistant.components.whois
whois==0.9.16 whois==0.9.16

View File

@ -1792,7 +1792,7 @@ wallbox==0.4.12
watchdog==2.2.0 watchdog==2.2.0
# homeassistant.components.whirlpool # homeassistant.components.whirlpool
whirlpool-sixth-sense==0.17.0 whirlpool-sixth-sense==0.17.1
# homeassistant.components.whois # homeassistant.components.whois
whois==0.9.16 whois==0.9.16

View File

@ -1,21 +1,29 @@
"""Tests for the Whirlpool Sixth Sense integration.""" """Tests for the Whirlpool Sixth Sense integration."""
from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.components.whirlpool.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def init_integration(hass: HomeAssistant) -> MockConfigEntry: async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfigEntry:
"""Set up the Whirlpool integration in Home Assistant.""" """Set up the Whirlpool integration in Home Assistant."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
CONF_USERNAME: "nobody", CONF_USERNAME: "nobody",
CONF_PASSWORD: "qwerty", CONF_PASSWORD: "qwerty",
CONF_REGION: region,
}, },
) )
return await init_integration_with_entry(hass, entry)
async def init_integration_with_entry(
hass: HomeAssistant, entry: MockConfigEntry
) -> MockConfigEntry:
"""Set up the Whirlpool integration in Home Assistant."""
entry.add_to_hass(hass) entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -5,11 +5,21 @@ from unittest.mock import AsyncMock
import pytest import pytest
import whirlpool import whirlpool
import whirlpool.aircon import whirlpool.aircon
from whirlpool.backendselector import Brand, Region
MOCK_SAID1 = "said1" MOCK_SAID1 = "said1"
MOCK_SAID2 = "said2" MOCK_SAID2 = "said2"
@pytest.fixture(
name="region",
params=[("EU", Region.EU, Brand.Whirlpool), ("US", Region.US, Brand.Maytag)],
)
def fixture_region(request):
"""Return a region for input."""
return request.param
@pytest.fixture(name="mock_auth_api") @pytest.fixture(name="mock_auth_api")
def fixture_mock_auth_api(): def fixture_mock_auth_api():
"""Set up Auth fixture.""" """Set up Auth fixture."""
@ -33,6 +43,15 @@ def fixture_mock_appliances_manager_api():
yield mock_appliances_manager yield mock_appliances_manager
@pytest.fixture(name="mock_backend_selector_api")
def fixture_mock_backend_selector_api():
"""Set up BackendSelector fixture."""
with mock.patch(
"homeassistant.components.whirlpool.BackendSelector"
) as mock_backend_selector:
yield mock_backend_selector
def get_aircon_mock(said): def get_aircon_mock(said):
"""Get a mock of an air conditioner.""" """Get a mock of an air conditioner."""
mock_aircon = mock.Mock(said=said) mock_aircon = mock.Mock(said=said)

View File

@ -13,8 +13,13 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
CONFIG_INPUT = {
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
}
async def test_form(hass):
async def test_form(hass, region):
"""Test we get the form.""" """Test we get the form."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -27,15 +32,14 @@ async def test_form(hass):
"homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid",
return_value=True, return_value=True,
), patch( ), patch(
"homeassistant.components.whirlpool.config_flow.BackendSelector"
) as mock_backend_selector, patch(
"homeassistant.components.whirlpool.async_setup_entry", "homeassistant.components.whirlpool.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
"username": "test-username",
"password": "test-password",
},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -44,11 +48,13 @@ async def test_form(hass):
assert result2["data"] == { assert result2["data"] == {
"username": "test-username", "username": "test-username",
"password": "test-password", "password": "test-password",
"region": region[0],
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
mock_backend_selector.assert_called_once_with(region[2], region[1])
async def test_form_invalid_auth(hass): async def test_form_invalid_auth(hass, region):
"""Test we handle invalid auth.""" """Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -59,16 +65,13 @@ async def test_form_invalid_auth(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"} assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass): async def test_form_cannot_connect(hass, region):
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -79,16 +82,13 @@ async def test_form_cannot_connect(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_auth_timeout(hass): async def test_form_auth_timeout(hass, region):
"""Test we handle auth timeout error.""" """Test we handle auth timeout error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -99,16 +99,13 @@ async def test_form_auth_timeout(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"} assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_generic_auth_exception(hass): async def test_form_generic_auth_exception(hass, region):
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -119,16 +116,13 @@ async def test_form_generic_auth_exception(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"} assert result2["errors"] == {"base": "unknown"}
async def test_form_already_configured(hass): async def test_form_already_configured(hass, region):
"""Test we handle cannot connect error.""" """Test we handle cannot connect error."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -150,10 +144,7 @@ async def test_form_already_configured(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ CONFIG_INPUT | {"region": region[0]},
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -161,12 +152,12 @@ async def test_form_already_configured(hass):
assert result2["reason"] == "already_configured" assert result2["reason"] == "already_configured"
async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_reauth_flow(hass: HomeAssistant, region) -> None:
"""Test a successful reauth flow.""" """Test a successful reauth flow."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username", unique_id="test-username",
) )
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)
@ -178,7 +169,11 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
"unique_id": mock_entry.unique_id, "unique_id": mock_entry.unique_id,
"entry_id": mock_entry.entry_id, "entry_id": mock_entry.entry_id,
}, },
data={"username": "test-username", "password": "new-password"}, data={
"username": "test-username",
"password": "new-password",
"region": region[0],
},
) )
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
@ -203,15 +198,16 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
assert mock_entry.data == { assert mock_entry.data == {
CONF_USERNAME: "test-username", CONF_USERNAME: "test-username",
CONF_PASSWORD: "new-password", CONF_PASSWORD: "new-password",
"region": region[0],
} }
async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None:
"""Test an authorization error reauth flow.""" """Test an authorization error reauth flow."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={"username": "test-username", "password": "test-password"}, data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username", unique_id="test-username",
) )
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)
@ -223,7 +219,11 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None:
"unique_id": mock_entry.unique_id, "unique_id": mock_entry.unique_id,
"entry_id": mock_entry.entry_id, "entry_id": mock_entry.entry_id,
}, },
data={"username": "test-username", "password": "new-password"}, data={
"username": "test-username",
"password": "new-password",
"region": region[0],
},
) )
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
@ -246,12 +246,12 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "invalid_auth"} assert result2["errors"] == {"base": "invalid_auth"}
async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: async def test_reauth_flow_connection_error(hass: HomeAssistant, region) -> None:
"""Test a connection error reauth flow.""" """Test a connection error reauth flow."""
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={"username": "test-username", "password": "test-password"}, data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username", unique_id="test-username",
) )
mock_entry.add_to_hass(hass) mock_entry.add_to_hass(hass)
@ -263,7 +263,11 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None:
"unique_id": mock_entry.unique_id, "unique_id": mock_entry.unique_id,
"entry_id": mock_entry.entry_id, "entry_id": mock_entry.entry_id,
}, },
data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, data={
CONF_USERNAME: "test-username",
CONF_PASSWORD: "new-password",
"region": region[0],
},
) )
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"

View File

@ -2,19 +2,44 @@
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
import aiohttp import aiohttp
from whirlpool.backendselector import Brand, Region
from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.components.whirlpool.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import init_integration from . import init_integration, init_integration_with_entry
from tests.common import MockConfigEntry
async def test_setup(hass: HomeAssistant): async def test_setup(hass: HomeAssistant, mock_backend_selector_api: MagicMock, region):
"""Test setup.""" """Test setup."""
entry = await init_integration(hass) entry = await init_integration(hass, region[0])
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
mock_backend_selector_api.assert_called_once_with(region[2], region[1])
async def test_setup_region_fallback(
hass: HomeAssistant, mock_backend_selector_api: MagicMock
):
"""Test setup when no region is available on the ConfigEntry.
This can happen after a version update, since there was no region in the first versions.
"""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: "nobody",
CONF_PASSWORD: "qwerty",
},
)
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.EU)
async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock): async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock):