Upgrade whirlpool integration to add shared appliances and allow brand selection (#111687)

* update to 1.18.5 and add Brand to config, required for getting shared appliances

* update version to 0.18.6

* start fixing tests

* fix typo

* check for falsy values instead of explicit None

* move CONF_BRAND from global constants to whirlpool constants

* add test for no brand, fix __init__ import

* add brand to string.json

* add brand to re-auth

* add title/description, add brand info to description

* add reauth strings

* pass already initialized data dict

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* remove trailing comma

* Update strings again

* fix reauth tests to add brand

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Jessica Smith 2024-03-19 20:02:45 -05:00 committed by GitHub
parent 9a38f0de0b
commit 02c1088596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 130 additions and 71 deletions

View file

@ -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
)

View file

@ -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")

View file

@ -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,
}

View file

@ -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"]
}

View file

@ -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

View file

@ -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"
}
}
},

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,
},
)

View file

@ -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."""

View file

@ -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()

View file

@ -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,