Update Whirlpool integration for 0.17.0 library (#76780)

* Update Whirlpool integration for 0.17.0 library

* Use dataclass for integration shared data
This commit is contained in:
Abílio Costa 2022-08-23 16:25:58 +01:00 committed by GitHub
parent 7f001cc1d1
commit b1d249c391
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 100 additions and 61 deletions

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
"""Constants for the Whirlpool Sixth Sense integration."""
DOMAIN = "whirlpool"
AUTH_INSTANCE_KEY = "auth"

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.15.1"],
"requirements": ["whirlpool-sixth-sense==0.17.0"],
"codeowners": ["@abmantis"],
"iot_class": "cloud_push",
"loggers": ["whirlpool"]

View File

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

View File

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

View File

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

View File

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

View File

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