diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 1991ec3806ce46737586f602452d765e8a7b74a1..c6721197abcc75f308b9a8b929417607781c8d7c 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,15 +1,18 @@ """The Whirlpool Sixth Sense integration.""" +from dataclasses import dataclass import logging import aiohttp +from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector, Brand, Region from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import AUTH_INSTANCE_KEY, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -20,7 +23,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) - auth = Auth(entry.data["username"], entry.data["password"]) + backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) + auth = Auth(backend_selector, entry.data["username"], entry.data["password"]) try: await auth.do_auth(store=False) except aiohttp.ClientError as ex: @@ -30,7 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Authentication failed") return False - hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth} + appliances_manager = AppliancesManager(backend_selector, auth) + if not await appliances_manager.fetch_appliances(): + _LOGGER.error("Cannot fetch appliances") + return False + + hass.data[DOMAIN][entry.entry_id] = WhirlpoolData( + appliances_manager, + auth, + backend_selector, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -44,3 +57,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@dataclass +class WhirlpoolData: + """Whirlpool integaration shared data.""" + + appliances_manager: AppliancesManager + auth: Auth + backend_selector: BackendSelector diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index c3786fd5f551f52167c46eafbfda099251eb1777..60de41b46f90a8b0d5dcc7e5fb341968e091ea1d 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -1,14 +1,14 @@ """Platform for climate integration.""" from __future__ import annotations -import asyncio import logging +from typing import Any -import aiohttp from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconMode from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( FAN_AUTO, FAN_HIGH, @@ -23,9 +23,11 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import AUTH_INSTANCE_KEY, DOMAIN +from . import WhirlpoolData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -67,14 +69,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up entry.""" - auth: Auth = hass.data[DOMAIN][config_entry.entry_id][AUTH_INSTANCE_KEY] - if not (said_list := auth.get_said_list()): - _LOGGER.debug("No appliances found") + whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + if not (aircons := whirlpool_data.appliances_manager.aircons): + _LOGGER.debug("No aircons found") return - # the whirlpool library needs to be updated to be able to support more - # than one device, so we use only the first one for now - aircons = [AirConEntity(said, auth) for said in said_list] + aircons = [ + AirConEntity( + hass, + ac_data["SAID"], + ac_data["NAME"], + whirlpool_data.backend_selector, + whirlpool_data.auth, + ) + for ac_data in aircons + ] async_add_entities(aircons, True) @@ -95,50 +104,44 @@ class AirConEntity(ClimateEntity): _attr_temperature_unit = TEMP_CELSIUS _attr_should_poll = False - def __init__(self, said, auth: Auth): + def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): """Initialize the entity.""" - self._aircon = Aircon(auth, said, self.async_write_ha_state) + self._aircon = Aircon(backend_selector, auth, said, self.async_write_ha_state) - self._attr_name = said + self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) + self._attr_name = name if name is not None else said self._attr_unique_id = said async def async_added_to_hass(self) -> None: """Connect aircon to the cloud.""" await self._aircon.connect() - try: - name = await self._aircon.fetch_name() - if name is not None: - self._attr_name = name - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Failed to get name") - @property def available(self) -> bool: """Return True if entity is available.""" return self._aircon.get_online() @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._aircon.get_current_temp() @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._aircon.get_temp() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._aircon.set_temp(kwargs.get(ATTR_TEMPERATURE)) @property - def current_humidity(self): + def current_humidity(self) -> int: """Return the current humidity.""" return self._aircon.get_current_humidity() @property - def target_humidity(self): + def target_humidity(self) -> int: """Return the humidity we try to reach.""" return self._aircon.get_humidity() @@ -169,30 +172,30 @@ class AirConEntity(ClimateEntity): await self._aircon.set_power_on(True) @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the fan setting.""" fanspeed = self._aircon.get_fanspeed() return AIRCON_FANSPEED_MAP.get(fanspeed, FAN_OFF) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" if not (fanspeed := FAN_MODE_TO_AIRCON_FANSPEED.get(fan_mode)): raise ValueError(f"Invalid fan mode {fan_mode}") await self._aircon.set_fanspeed(fanspeed) @property - def swing_mode(self): + def swing_mode(self) -> str: """Return the swing setting.""" return SWING_HORIZONTAL if self._aircon.get_h_louver_swing() else SWING_OFF - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target temperature.""" await self._aircon.set_h_louver_swing(swing_mode == SWING_HORIZONTAL) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._aircon.set_power_on(True) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._aircon.set_power_on(False) diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index ac6cb3d568e4e2f42f66dbf8cf3883ad01be3032..dbc59f8241635b72ba9deafa9735f8d2b6ae30ed 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -7,9 +7,11 @@ import logging import aiohttp import voluptuous as vol from whirlpool.auth import Auth +from whirlpool.backendselector import BackendSelector, Brand, Region from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -27,7 +29,8 @@ async def validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - auth = Auth(data[CONF_USERNAME], data[CONF_PASSWORD]) + backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) + auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) try: await auth.do_auth() except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as exc: @@ -44,7 +47,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index 16ba293e3b228b3c89d38c95166387d6a4b4dbd7..8a030d8fab2d28f94ed7d70bd4764306db295a50 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,4 +1,3 @@ """Constants for the Whirlpool Sixth Sense integration.""" DOMAIN = "whirlpool" -AUTH_INSTANCE_KEY = "auth" diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 4c7471e4715542dfc305a40ea40f26f2e7ec0fd3..a7c99e9066cc611aabe473f0b9e84b0a48f72e22 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -3,7 +3,7 @@ "name": "Whirlpool Sixth Sense", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.15.1"], + "requirements": ["whirlpool-sixth-sense==0.17.0"], "codeowners": ["@abmantis"], "iot_class": "cloud_push", "loggers": ["whirlpool"] diff --git a/requirements_all.txt b/requirements_all.txt index 56e2fdd72c8731087d6934dac37582d977437872..95afa8a04f1ae174376c521217ee2ba7ef2875d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.15.1 +whirlpool-sixth-sense==0.17.0 # homeassistant.components.whois whois==0.9.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c97130ee6aafebf721e382b348557232f376a07b..a558bbe5b28c18ebec1dfe96bbfe7e484c64db84 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1675,7 +1675,7 @@ wallbox==0.4.9 watchdog==2.1.9 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.15.1 +whirlpool-sixth-sense==0.17.0 # homeassistant.components.whois whois==0.9.16 diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index 3a5fd0e3d2e9625b45ee55866a828995791faa8c..eba58b07faa3da79ba69dff3764d900bfc06154c 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -12,19 +12,31 @@ MOCK_SAID2 = "said2" @pytest.fixture(name="mock_auth_api") def fixture_mock_auth_api(): - """Set up air conditioner Auth fixture.""" + """Set up Auth fixture.""" with mock.patch("homeassistant.components.whirlpool.Auth") as mock_auth: mock_auth.return_value.do_auth = AsyncMock() mock_auth.return_value.is_access_token_valid.return_value = True - mock_auth.return_value.get_said_list.return_value = [MOCK_SAID1, MOCK_SAID2] yield mock_auth +@pytest.fixture(name="mock_appliances_manager_api") +def fixture_mock_appliances_manager_api(): + """Set up AppliancesManager fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.AppliancesManager" + ) as mock_appliances_manager: + mock_appliances_manager.return_value.fetch_appliances = AsyncMock() + mock_appliances_manager.return_value.aircons = [ + {"SAID": MOCK_SAID1, "NAME": "TestZone"}, + {"SAID": MOCK_SAID2, "NAME": "TestZone"}, + ] + yield mock_appliances_manager + + def get_aircon_mock(said): """Get a mock of an air conditioner.""" mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() - mock_aircon.fetch_name = AsyncMock(return_value="TestZone") mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -47,13 +59,13 @@ def get_aircon_mock(said): @pytest.fixture(name="mock_aircon1_api", autouse=True) -def fixture_mock_aircon1_api(mock_auth_api): +def fixture_mock_aircon1_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID1) @pytest.fixture(name="mock_aircon2_api", autouse=True) -def fixture_mock_aircon2_api(mock_auth_api): +def fixture_mock_aircon2_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID2) diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index a1f2ed38aba9c9f14ae60ca8fe7f75bbd4bdb46c..f0d4c93f8d692c96e43fec4b80bf25143bf84319 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -1,7 +1,6 @@ """Test the Whirlpool Sixth Sense climate domain.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock -import aiohttp from attr import dataclass import pytest import whirlpool @@ -58,30 +57,21 @@ async def update_ac_state( """Simulate an update trigger from the API.""" update_ha_state_cb = mock_aircon_api_instances.call_args_list[ mock_instance_idx - ].args[2] + ].args[3] update_ha_state_cb() await hass.async_block_till_done() return hass.states.get(entity_id) -async def test_no_appliances(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_no_appliances( + hass: HomeAssistant, mock_appliances_manager_api: MagicMock +): """Test the setup of the climate entities when there are no appliances available.""" - mock_auth_api.return_value.get_said_list.return_value = [] + mock_appliances_manager_api.return_value.aircons = [] await init_integration(hass) assert len(hass.states.async_all()) == 0 -async def test_name_fallback_on_exception( - hass: HomeAssistant, mock_aircon1_api: MagicMock -): - """Test name property.""" - mock_aircon1_api.fetch_name = AsyncMock(side_effect=aiohttp.ClientError()) - - await init_integration(hass) - state = hass.states.get("climate.said1") - assert state.attributes[ATTR_FRIENDLY_NAME] == "said1" - - async def test_static_attributes(hass: HomeAssistant, mock_aircon1_api: MagicMock): """Test static climate attributes.""" await init_integration(hass) diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 00fc27ddc63802e9d3326dd20cd4f3c51b6da893..626c127b61af5c8a74896ad09f800a13004fabad 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -36,6 +36,16 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): assert entry.state is ConfigEntryState.SETUP_ERROR +async def test_setup_fetch_appliances_failed( + hass: HomeAssistant, mock_appliances_manager_api: MagicMock +): + """Test setup with failed fetch_appliances.""" + mock_appliances_manager_api.return_value.fetch_appliances.return_value = False + entry = await init_integration(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.SETUP_ERROR + + async def test_unload_entry(hass: HomeAssistant): """Test successful unload of entry.""" entry = await init_integration(hass)