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
from whirlpool.appliancesmanager import AppliancesManager
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.const import Platform
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
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__)
@ -23,8 +24,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Whirlpool Sixth Sense from a config entry."""
hass.data.setdefault(DOMAIN, {})
backend_selector = BackendSelector(Brand.Whirlpool, Region.EU)
auth = Auth(backend_selector, entry.data["username"], entry.data["password"])
region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")]
brand = get_brand_for_region(region)
backend_selector = BackendSelector(brand, region)
auth = Auth(backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
try:
await auth.do_auth(store=False)
except aiohttp.ClientError as ex:

View File

@ -9,18 +9,24 @@ from typing import Any
import aiohttp
import voluptuous as vol
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.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
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__)
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})
@ -33,7 +39,9 @@ async def validate_input(
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])
try:
await auth.do_auth()
@ -68,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self.entry is not None
password = user_input[CONF_PASSWORD]
data = {
CONF_USERNAME: self.entry.data[CONF_USERNAME],
**self.entry.data,
CONF_PASSWORD: password,
}

View File

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

View File

@ -3,7 +3,7 @@
"name": "Whirlpool Sixth Sense",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/whirlpool",
"requirements": ["whirlpool-sixth-sense==0.17.0"],
"requirements": ["whirlpool-sixth-sense==0.17.1"],
"codeowners": ["@abmantis"],
"iot_class": "cloud_push",
"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
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.17.0
whirlpool-sixth-sense==0.17.1
# homeassistant.components.whois
whois==0.9.16

View File

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

View File

@ -1,21 +1,29 @@
"""Tests for the Whirlpool Sixth Sense integration."""
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 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."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: "nobody",
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)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

View File

@ -5,11 +5,21 @@ from unittest.mock import AsyncMock
import pytest
import whirlpool
import whirlpool.aircon
from whirlpool.backendselector import Brand, Region
MOCK_SAID1 = "said1"
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")
def fixture_mock_auth_api():
"""Set up Auth fixture."""
@ -33,6 +43,15 @@ def fixture_mock_appliances_manager_api():
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):
"""Get a mock of an air conditioner."""
mock_aircon = mock.Mock(said=said)

View File

@ -13,8 +13,13 @@ from homeassistant.data_entry_flow import FlowResultType
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."""
result = await hass.config_entries.flow.async_init(
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",
return_value=True,
), patch(
"homeassistant.components.whirlpool.config_flow.BackendSelector"
) as mock_backend_selector, patch(
"homeassistant.components.whirlpool.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
await hass.async_block_till_done()
@ -44,11 +48,13 @@ async def test_form(hass):
assert result2["data"] == {
"username": "test-username",
"password": "test-password",
"region": region[0],
}
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."""
result = await hass.config_entries.flow.async_init(
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(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
assert result2["type"] == "form"
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."""
result = await hass.config_entries.flow.async_init(
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(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
assert result2["type"] == "form"
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."""
result = await hass.config_entries.flow.async_init(
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(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
assert result2["type"] == "form"
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."""
result = await hass.config_entries.flow.async_init(
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(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
assert result2["type"] == "form"
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."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
@ -150,10 +144,7 @@ async def test_form_already_configured(hass):
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
CONFIG_INPUT | {"region": region[0]},
)
await hass.async_block_till_done()
@ -161,12 +152,12 @@ async def test_form_already_configured(hass):
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."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"},
data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username",
)
mock_entry.add_to_hass(hass)
@ -178,7 +169,11 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
"unique_id": mock_entry.unique_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"
@ -203,15 +198,16 @@ async def test_reauth_flow(hass: HomeAssistant) -> None:
assert mock_entry.data == {
CONF_USERNAME: "test-username",
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."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={"username": "test-username", "password": "test-password"},
data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username",
)
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,
"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"
@ -246,12 +246,12 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None:
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."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={"username": "test-username", "password": "test-password"},
data=CONFIG_INPUT | {"region": region[0]},
unique_id="test-username",
)
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,
"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"

View File

@ -2,19 +2,44 @@
from unittest.mock import AsyncMock, MagicMock
import aiohttp
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.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."""
entry = await init_integration(hass)
entry = await init_integration(hass, region[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])
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):