mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
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:
parent
7f001cc1d1
commit
b1d249c391
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -1,4 +1,3 @@
|
||||
"""Constants for the Whirlpool Sixth Sense integration."""
|
||||
|
||||
DOMAIN = "whirlpool"
|
||||
AUTH_INSTANCE_KEY = "auth"
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user