Add Whirlpool integration (#48346)

* Add Whirlpool integration

* Apply suggestions from code review

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Apply suggestions from code review

* Fix lint

* Fix lint and tests

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Use dict lookups

* Lint

* Apply code changes from PR review

* Do real integration setup in tests

* Apply suggestions from review & fix test

* Replace get with array operator

* Add suggestions from code review

* Rename test var

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Abílio Costa
2021-09-13 21:02:34 +01:00
committed by GitHub
parent f023ec24d7
commit c869b78ac1
16 changed files with 973 additions and 0 deletions

View File

@ -576,6 +576,7 @@ homeassistant/components/weather/* @fabaff
homeassistant/components/webostv/* @bendavid @thecode
homeassistant/components/websocket_api/* @home-assistant/core
homeassistant/components/wemo/* @esev
homeassistant/components/whirlpool/* @abmantis
homeassistant/components/wiffi/* @mampfes
homeassistant/components/wilight/* @leofig-rj
homeassistant/components/wirelesstag/* @sergeymaysak

View File

@ -0,0 +1,45 @@
"""The Whirlpool Sixth Sense integration."""
import logging
import aiohttp
from whirlpool.auth import Auth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import AUTH_INSTANCE_KEY, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["climate"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Whirlpool Sixth Sense from a config entry."""
hass.data.setdefault(DOMAIN, {})
auth = Auth(entry.data["username"], entry.data["password"])
try:
await auth.do_auth(store=False)
except aiohttp.ClientError as ex:
raise ConfigEntryNotReady("Cannot connect") from ex
if not auth.is_access_token_valid():
_LOGGER.error("Authentication failed")
return False
hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth}
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,189 @@
"""Platform for climate integration."""
import asyncio
import logging
import aiohttp
from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconMode
from whirlpool.auth import Auth
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
HVAC_MODE_COOL,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_FAN_MODE,
SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE,
SWING_HORIZONTAL,
SWING_OFF,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from .const import AUTH_INSTANCE_KEY, DOMAIN
_LOGGER = logging.getLogger(__name__)
AIRCON_MODE_MAP = {
AirconMode.Cool: HVAC_MODE_COOL,
AirconMode.Heat: HVAC_MODE_HEAT,
AirconMode.Fan: HVAC_MODE_FAN_ONLY,
}
HVAC_MODE_TO_AIRCON_MODE = {v: k for k, v in AIRCON_MODE_MAP.items()}
AIRCON_FANSPEED_MAP = {
AirconFanSpeed.Off: FAN_OFF,
AirconFanSpeed.Auto: FAN_AUTO,
AirconFanSpeed.Low: FAN_LOW,
AirconFanSpeed.Medium: FAN_MEDIUM,
AirconFanSpeed.High: FAN_HIGH,
}
FAN_MODE_TO_AIRCON_FANSPEED = {v: k for k, v in AIRCON_FANSPEED_MAP.items()}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up entry."""
auth: Auth = hass.data[DOMAIN][config_entry.entry_id][AUTH_INSTANCE_KEY]
said_list = auth.get_said_list()
if not said_list:
_LOGGER.debug("No appliances 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
aircon = AirConEntity(said_list[0], auth)
async_add_entities([aircon], True)
class AirConEntity(ClimateEntity):
"""Representation of an air conditioner."""
_attr_fan_modes = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW, FAN_OFF]
_attr_hvac_modes = [
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
]
_attr_max_temp = 30
_attr_min_temp = 16
_attr_supported_features = (
SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE
)
_attr_swing_modes = [SWING_HORIZONTAL, SWING_OFF]
_attr_target_temperature_step = 1
_attr_temperature_unit = TEMP_CELSIUS
_attr_should_poll = False
def __init__(self, said, auth: Auth):
"""Initialize the entity."""
self._aircon = Aircon(auth, said, self.async_write_ha_state)
self._attr_name = 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):
"""Return the current temperature."""
return self._aircon.get_current_temp()
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._aircon.get_temp()
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
await self._aircon.set_temp(kwargs.get(ATTR_TEMPERATURE))
@property
def current_humidity(self):
"""Return the current humidity."""
return self._aircon.get_current_humidity()
@property
def target_humidity(self):
"""Return the humidity we try to reach."""
return self._aircon.get_humidity()
async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
await self._aircon.set_humidity(humidity)
@property
def hvac_mode(self):
"""Return current operation ie. heat, cool, fan."""
if not self._aircon.get_power_on():
return HVAC_MODE_OFF
mode: AirconMode = self._aircon.get_mode()
return AIRCON_MODE_MAP.get(mode, None)
async def async_set_hvac_mode(self, hvac_mode):
"""Set HVAC mode."""
if hvac_mode == HVAC_MODE_OFF:
await self._aircon.set_power_on(False)
return
mode = HVAC_MODE_TO_AIRCON_MODE.get(hvac_mode)
if not mode:
_LOGGER.warning("Unexpected hvac mode: %s", hvac_mode)
return
await self._aircon.set_mode(mode)
if not self._aircon.get_power_on():
await self._aircon.set_power_on(True)
@property
def fan_mode(self):
"""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):
"""Set fan mode."""
fanspeed = FAN_MODE_TO_AIRCON_FANSPEED.get(fan_mode)
if not fanspeed:
return
await self._aircon.set_fanspeed(fanspeed)
@property
def swing_mode(self):
"""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):
"""Set new target temperature."""
await self._aircon.set_h_louver_swing(swing_mode == SWING_HORIZONTAL)
async def async_turn_on(self):
"""Turn device on."""
await self._aircon.set_power_on(True)
async def async_turn_off(self):
"""Turn device off."""
await self._aircon.set_power_on(False)

View File

@ -0,0 +1,76 @@
"""Config flow for Whirlpool Sixth Sense integration."""
import asyncio
import logging
import aiohttp
import voluptuous as vol
from whirlpool.auth import Auth
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN # pylint: disable=unused-import
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str})
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
auth = Auth(data[CONF_USERNAME], data[CONF_PASSWORD])
try:
await auth.do_auth()
except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as exc:
raise CannotConnect from exc
if not auth.is_access_token_valid():
raise InvalidAuth
return {"title": data[CONF_USERNAME]}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Whirlpool Sixth Sense."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
user_input[CONF_USERNAME].lower(), raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

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

View File

@ -0,0 +1,13 @@
{
"domain": "whirlpool",
"name": "Whirlpool Sixth Sense",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/whirlpool",
"requirements": [
"whirlpool-sixth-sense==0.15.1"
],
"codeowners": [
"@abmantis"
],
"iot_class": "cloud_push"
}

View File

@ -0,0 +1,17 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View File

@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Password",
"username": "Username"
}
}
}
},
"title": "Whirlpool Sixth Sense"
}

View File

@ -302,6 +302,7 @@ FLOWS = [
"wallbox",
"waze_travel_time",
"wemo",
"whirlpool",
"wiffi",
"wilight",
"withings",

View File

@ -2390,6 +2390,9 @@ waterfurnace==1.1.0
# homeassistant.components.cisco_webex_teams
webexteamssdk==1.1.1
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.15.1
# homeassistant.components.wiffi
wiffi==1.0.1

View File

@ -1337,6 +1337,9 @@ wallbox==0.4.4
# homeassistant.components.folder_watcher
watchdog==2.1.4
# homeassistant.components.whirlpool
whirlpool-sixth-sense==0.15.1
# homeassistant.components.wiffi
wiffi==1.0.1

View File

@ -0,0 +1,23 @@
"""Tests for the Whirlpool Sixth Sense integration."""
from homeassistant.components.whirlpool.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
"""Set up the Whirlpool integration in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: "nobody",
CONF_PASSWORD: "qwerty",
},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@ -0,0 +1,41 @@
"""Fixtures for the Whirlpool Sixth Sense integration tests."""
from unittest import mock
from unittest.mock import AsyncMock
import pytest
import whirlpool
MOCK_SAID = "said1"
@pytest.fixture(name="mock_auth_api")
def fixture_mock_auth_api():
"""Set up air conditioner 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_SAID]
yield mock_auth
@pytest.fixture(name="mock_aircon_api", autouse=True)
def fixture_mock_aircon_api(mock_auth_api):
"""Set up air conditioner API fixture."""
with mock.patch(
"homeassistant.components.whirlpool.climate.Aircon"
) as mock_aircon_api:
mock_aircon_api.return_value.connect = AsyncMock()
mock_aircon_api.return_value.fetch_name = AsyncMock(return_value="TestZone")
mock_aircon_api.return_value.said = MOCK_SAID
mock_aircon_api.return_value.get_online.return_value = True
mock_aircon_api.return_value.get_power_on.return_value = True
mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Cool
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.Auto
)
mock_aircon_api.return_value.get_current_temp.return_value = 15
mock_aircon_api.return_value.get_temp.return_value = 20
mock_aircon_api.return_value.get_current_humidity.return_value = 80
mock_aircon_api.return_value.get_humidity.return_value = 50
mock_aircon_api.return_value.get_h_louver_swing.return_value = True
yield mock_aircon_api

View File

@ -0,0 +1,364 @@
"""Test the Whirlpool Sixth Sense climate domain."""
from unittest.mock import AsyncMock, MagicMock
import aiohttp
import whirlpool
from homeassistant.components.climate.const import (
ATTR_CURRENT_HUMIDITY,
ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE,
ATTR_FAN_MODES,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_SWING_MODE,
ATTR_SWING_MODES,
ATTR_TARGET_TEMP_STEP,
DOMAIN as CLIMATE_DOMAIN,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_MIDDLE,
FAN_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_SWING_MODE,
SERVICE_SET_TEMPERATURE,
SUPPORT_FAN_MODE,
SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE,
SWING_HORIZONTAL,
SWING_OFF,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import init_integration
async def update_ac_state(hass: HomeAssistant, mock_aircon_api: MagicMock):
"""Simulate an update trigger from the API."""
update_ha_state_cb = mock_aircon_api.call_args.args[2]
update_ha_state_cb()
await hass.async_block_till_done()
return hass.states.get("climate.said1")
async def test_no_appliances(hass: HomeAssistant, mock_auth_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 = []
await init_integration(hass)
assert len(hass.states.async_all()) == 0
async def test_name_fallback_on_exception(
hass: HomeAssistant, mock_aircon_api: MagicMock
):
"""Test name property."""
mock_aircon_api.return_value.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_aircon_api: MagicMock):
"""Test static climate attributes."""
await init_integration(hass)
entry = er.async_get(hass).async_get("climate.said1")
assert entry
assert entry.unique_id == "said1"
state = hass.states.get("climate.said1")
assert state is not None
assert state.state != STATE_UNAVAILABLE
assert state.state == HVAC_MODE_COOL
attributes = state.attributes
assert attributes[ATTR_FRIENDLY_NAME] == "TestZone"
assert (
attributes[ATTR_SUPPORTED_FEATURES]
== SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE
)
assert attributes[ATTR_HVAC_MODES] == [
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_OFF,
]
assert attributes[ATTR_FAN_MODES] == [
FAN_AUTO,
FAN_HIGH,
FAN_MEDIUM,
FAN_LOW,
FAN_OFF,
]
assert attributes[ATTR_SWING_MODES] == [SWING_HORIZONTAL, SWING_OFF]
assert attributes[ATTR_TARGET_TEMP_STEP] == 1
assert attributes[ATTR_MIN_TEMP] == 16
assert attributes[ATTR_MAX_TEMP] == 30
async def test_dynamic_attributes(hass: HomeAssistant, mock_aircon_api: MagicMock):
"""Test dynamic attributes."""
await init_integration(hass)
state = hass.states.get("climate.said1")
assert state is not None
assert state.state == HVAC_MODE_COOL
mock_aircon_api.return_value.get_power_on.return_value = False
state = await update_ac_state(hass, mock_aircon_api)
assert state.state == HVAC_MODE_OFF
mock_aircon_api.return_value.get_online.return_value = False
state = await update_ac_state(hass, mock_aircon_api)
assert state.state == STATE_UNAVAILABLE
mock_aircon_api.return_value.get_power_on.return_value = True
mock_aircon_api.return_value.get_online.return_value = True
state = await update_ac_state(hass, mock_aircon_api)
assert state.state == HVAC_MODE_COOL
mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Heat
state = await update_ac_state(hass, mock_aircon_api)
assert state.state == HVAC_MODE_HEAT
mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Fan
state = await update_ac_state(hass, mock_aircon_api)
assert state.state == HVAC_MODE_FAN_ONLY
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.Auto
)
state = await update_ac_state(hass, mock_aircon_api)
assert state.attributes[ATTR_FAN_MODE] == HVAC_MODE_AUTO
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.Low
)
state = await update_ac_state(hass, mock_aircon_api)
assert state.attributes[ATTR_FAN_MODE] == FAN_LOW
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.Medium
)
state = await update_ac_state(hass, mock_aircon_api)
assert state.attributes[ATTR_FAN_MODE] == FAN_MEDIUM
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.High
)
state = await update_ac_state(hass, mock_aircon_api)
assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH
mock_aircon_api.return_value.get_fanspeed.return_value = (
whirlpool.aircon.FanSpeed.Off
)
state = await update_ac_state(hass, mock_aircon_api)
assert state.attributes[ATTR_FAN_MODE] == FAN_OFF
mock_aircon_api.return_value.get_current_temp.return_value = 15
mock_aircon_api.return_value.get_temp.return_value = 20
mock_aircon_api.return_value.get_current_humidity.return_value = 80
mock_aircon_api.return_value.get_h_louver_swing.return_value = True
attributes = (await update_ac_state(hass, mock_aircon_api)).attributes
assert attributes[ATTR_CURRENT_TEMPERATURE] == 15
assert attributes[ATTR_TEMPERATURE] == 20
assert attributes[ATTR_CURRENT_HUMIDITY] == 80
assert attributes[ATTR_SWING_MODE] == SWING_HORIZONTAL
mock_aircon_api.return_value.get_current_temp.return_value = 16
mock_aircon_api.return_value.get_temp.return_value = 21
mock_aircon_api.return_value.get_current_humidity.return_value = 70
mock_aircon_api.return_value.get_h_louver_swing.return_value = False
attributes = (await update_ac_state(hass, mock_aircon_api)).attributes
assert attributes[ATTR_CURRENT_TEMPERATURE] == 16
assert attributes[ATTR_TEMPERATURE] == 21
assert attributes[ATTR_CURRENT_HUMIDITY] == 70
assert attributes[ATTR_SWING_MODE] == SWING_OFF
async def test_service_calls(hass: HomeAssistant, mock_aircon_api: MagicMock):
"""Test controlling the entity through service calls."""
await init_integration(hass)
mock_aircon_api.return_value.set_power_on = AsyncMock()
mock_aircon_api.return_value.set_mode = AsyncMock()
mock_aircon_api.return_value.set_temp = AsyncMock()
mock_aircon_api.return_value.set_humidity = AsyncMock()
mock_aircon_api.return_value.set_mode = AsyncMock()
mock_aircon_api.return_value.set_fanspeed = AsyncMock()
mock_aircon_api.return_value.set_h_louver_swing = AsyncMock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "climate.said1"},
blocking=True,
)
mock_aircon_api.return_value.set_power_on.assert_called_once_with(False)
mock_aircon_api.return_value.set_power_on.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "climate.said1"},
blocking=True,
)
mock_aircon_api.return_value.set_power_on.assert_called_once_with(True)
mock_aircon_api.return_value.set_power_on.reset_mock()
mock_aircon_api.return_value.get_power_on.return_value = False
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_COOL},
blocking=True,
)
mock_aircon_api.return_value.set_power_on.assert_called_once_with(True)
mock_aircon_api.return_value.set_temp.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_TEMPERATURE: 15},
blocking=True,
)
mock_aircon_api.return_value.set_temp.assert_called_once_with(15)
mock_aircon_api.return_value.set_mode.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_COOL},
blocking=True,
)
mock_aircon_api.return_value.set_mode.assert_called_once_with(
whirlpool.aircon.Mode.Cool
)
mock_aircon_api.return_value.set_mode.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
blocking=True,
)
mock_aircon_api.return_value.set_mode.assert_called_once_with(
whirlpool.aircon.Mode.Heat
)
mock_aircon_api.return_value.set_mode.reset_mock()
# HVAC_MODE_DRY should be ignored
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_DRY},
blocking=True,
)
mock_aircon_api.return_value.set_mode.assert_not_called()
mock_aircon_api.return_value.set_mode.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_FAN_ONLY},
blocking=True,
)
mock_aircon_api.return_value.set_mode.assert_called_once_with(
whirlpool.aircon.Mode.Fan
)
mock_aircon_api.return_value.set_fanspeed.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_AUTO},
blocking=True,
)
mock_aircon_api.return_value.set_fanspeed.assert_called_once_with(
whirlpool.aircon.FanSpeed.Auto
)
mock_aircon_api.return_value.set_fanspeed.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_LOW},
blocking=True,
)
mock_aircon_api.return_value.set_fanspeed.assert_called_once_with(
whirlpool.aircon.FanSpeed.Low
)
mock_aircon_api.return_value.set_fanspeed.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MEDIUM},
blocking=True,
)
mock_aircon_api.return_value.set_fanspeed.assert_called_once_with(
whirlpool.aircon.FanSpeed.Medium
)
mock_aircon_api.return_value.set_fanspeed.reset_mock()
# FAN_MIDDLE should be ignored
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MIDDLE},
blocking=True,
)
mock_aircon_api.return_value.set_fanspeed.assert_not_called()
mock_aircon_api.return_value.set_fanspeed.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_HIGH},
blocking=True,
)
mock_aircon_api.return_value.set_fanspeed.assert_called_once_with(
whirlpool.aircon.FanSpeed.High
)
mock_aircon_api.return_value.set_h_louver_swing.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_SWING_MODE: SWING_HORIZONTAL},
blocking=True,
)
mock_aircon_api.return_value.set_h_louver_swing.assert_called_with(True)
mock_aircon_api.return_value.set_h_louver_swing.reset_mock()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_SWING_MODE,
{ATTR_ENTITY_ID: "climate.said1", ATTR_SWING_MODE: SWING_OFF},
blocking=True,
)
mock_aircon_api.return_value.set_h_louver_swing.assert_called_with(False)

View File

@ -0,0 +1,122 @@
"""Test the Whirlpool Sixth Sense config flow."""
import asyncio
from unittest.mock import patch
import aiohttp
from homeassistant import config_entries
from homeassistant.components.whirlpool.const import DOMAIN
async def test_form(hass):
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == config_entries.SOURCE_USER
with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch(
"homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid",
return_value=True,
), 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",
},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "test-username"
assert result2["data"] == {
"username": "test-username",
"password": "test-password",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch(
"homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid",
return_value=False,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.whirlpool.config_flow.Auth.do_auth",
side_effect=aiohttp.ClientConnectionError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_auth_timeout(hass):
"""Test we handle auth timeout error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.whirlpool.config_flow.Auth.do_auth",
side_effect=asyncio.TimeoutError,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_generic_auth_exception(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.whirlpool.config_flow.Auth.do_auth",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}

View File

@ -0,0 +1,49 @@
"""Test the Whirlpool Sixth Sense init."""
from unittest.mock import AsyncMock, MagicMock
import aiohttp
from homeassistant.components.whirlpool.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.components.whirlpool import init_integration
async def test_setup(hass: HomeAssistant):
"""Test setup."""
entry = await init_integration(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.LOADED
async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock):
"""Test setup with an http exception."""
mock_auth_api.return_value.do_auth = AsyncMock(
side_effect=aiohttp.ClientConnectionError()
)
entry = await init_integration(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock):
"""Test setup with failed auth."""
mock_auth_api.return_value.do_auth = AsyncMock()
mock_auth_api.return_value.is_access_token_valid.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)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)