mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
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:
@ -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
|
||||
|
45
homeassistant/components/whirlpool/__init__.py
Normal file
45
homeassistant/components/whirlpool/__init__.py
Normal 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
|
189
homeassistant/components/whirlpool/climate.py
Normal file
189
homeassistant/components/whirlpool/climate.py
Normal 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)
|
76
homeassistant/components/whirlpool/config_flow.py
Normal file
76
homeassistant/components/whirlpool/config_flow.py
Normal 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."""
|
4
homeassistant/components/whirlpool/const.py
Normal file
4
homeassistant/components/whirlpool/const.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Constants for the Whirlpool Sixth Sense integration."""
|
||||
|
||||
DOMAIN = "whirlpool"
|
||||
AUTH_INSTANCE_KEY = "auth"
|
13
homeassistant/components/whirlpool/manifest.json
Normal file
13
homeassistant/components/whirlpool/manifest.json
Normal 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"
|
||||
}
|
17
homeassistant/components/whirlpool/strings.json
Normal file
17
homeassistant/components/whirlpool/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
22
homeassistant/components/whirlpool/translations/en.json
Normal file
22
homeassistant/components/whirlpool/translations/en.json
Normal 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"
|
||||
}
|
@ -302,6 +302,7 @@ FLOWS = [
|
||||
"wallbox",
|
||||
"waze_travel_time",
|
||||
"wemo",
|
||||
"whirlpool",
|
||||
"wiffi",
|
||||
"wilight",
|
||||
"withings",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
23
tests/components/whirlpool/__init__.py
Normal file
23
tests/components/whirlpool/__init__.py
Normal 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
|
41
tests/components/whirlpool/conftest.py
Normal file
41
tests/components/whirlpool/conftest.py
Normal 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
|
364
tests/components/whirlpool/test_climate.py
Normal file
364
tests/components/whirlpool/test_climate.py
Normal 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)
|
122
tests/components/whirlpool/test_config_flow.py
Normal file
122
tests/components/whirlpool/test_config_flow.py
Normal 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"}
|
49
tests/components/whirlpool/test_init.py
Normal file
49
tests/components/whirlpool/test_init.py
Normal 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)
|
Reference in New Issue
Block a user