Add humidifier entity for Vesync devices (#134333)

This commit is contained in:
Indu Prakash 2025-01-13 12:26:18 -06:00 committed by GitHub
parent 6fd73730cc
commit d986fe7a07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 799 additions and 16 deletions

View File

@ -20,7 +20,13 @@ from .const import (
)
from .coordinator import VeSyncDataCoordinator
PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [
Platform.FAN,
Platform.HUMIDIFIER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)

View File

@ -7,6 +7,8 @@ from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.core import HomeAssistant
from .const import VeSyncHumidifierDevice
_LOGGER = logging.getLogger(__name__)
@ -24,3 +26,9 @@ async def async_generate_device_list(
devices.extend(manager.switches)
return devices
def is_humidifier(device: VeSyncBaseDevice) -> bool:
"""Check if the device represents a humidifier."""
return isinstance(device, VeSyncHumidifierDevice)

View File

@ -1,5 +1,7 @@
"""Constants for VeSync Component."""
from pyvesync.vesyncfan import VeSyncHumid200300S, VeSyncSuperior6000S
DOMAIN = "vesync"
VS_DISCOVERY = "vesync_discovery_{}"
SERVICE_UPDATE_DEVS = "update_devices"
@ -21,6 +23,14 @@ VS_DEVICES = "devices"
VS_COORDINATOR = "coordinator"
VS_MANAGER = "manager"
VS_HUMIDIFIER_MODE_AUTO = "auto"
VS_HUMIDIFIER_MODE_HUMIDITY = "humidity"
VS_HUMIDIFIER_MODE_MANUAL = "manual"
VS_HUMIDIFIER_MODE_SLEEP = "sleep"
VeSyncHumidifierDevice = VeSyncHumid200300S | VeSyncSuperior6000S
"""Humidifier device types"""
DEV_TYPE_TO_HA = {
"wifi-switch-1.3": "outlet",
"ESW03-USA": "outlet",

View File

@ -0,0 +1,161 @@
"""Support for VeSync humidifiers."""
import logging
from typing import Any
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from homeassistant.components.humidifier import (
ATTR_HUMIDITY,
MODE_AUTO,
MODE_NORMAL,
MODE_SLEEP,
HumidifierEntity,
HumidifierEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .common import is_humidifier
from .const import (
DOMAIN,
VS_COORDINATOR,
VS_DEVICES,
VS_DISCOVERY,
VS_HUMIDIFIER_MODE_AUTO,
VS_HUMIDIFIER_MODE_HUMIDITY,
VS_HUMIDIFIER_MODE_MANUAL,
VS_HUMIDIFIER_MODE_SLEEP,
VeSyncHumidifierDevice,
)
from .coordinator import VeSyncDataCoordinator
from .entity import VeSyncBaseEntity
_LOGGER = logging.getLogger(__name__)
MIN_HUMIDITY = 30
MAX_HUMIDITY = 80
VS_TO_HA_ATTRIBUTES = {ATTR_HUMIDITY: "current_humidity"}
VS_TO_HA_MODE_MAP = {
VS_HUMIDIFIER_MODE_AUTO: MODE_AUTO,
VS_HUMIDIFIER_MODE_HUMIDITY: MODE_AUTO,
VS_HUMIDIFIER_MODE_MANUAL: MODE_NORMAL,
VS_HUMIDIFIER_MODE_SLEEP: MODE_SLEEP,
}
HA_TO_VS_MODE_MAP = {v: k for k, v in VS_TO_HA_MODE_MAP.items()}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the VeSync humidifier platform."""
coordinator = hass.data[DOMAIN][VS_COORDINATOR]
@callback
def discover(devices):
"""Add new devices to platform."""
_setup_entities(devices, async_add_entities, coordinator)
config_entry.async_on_unload(
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_DEVICES), discover)
)
_setup_entities(hass.data[DOMAIN][VS_DEVICES], async_add_entities, coordinator)
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities: AddEntitiesCallback,
coordinator: VeSyncDataCoordinator,
):
"""Add humidifier entities."""
async_add_entities(
VeSyncHumidifierHA(dev, coordinator) for dev in devices if is_humidifier(dev)
)
def _get_ha_mode(vs_mode: str) -> str | None:
ha_mode = VS_TO_HA_MODE_MAP.get(vs_mode)
if ha_mode is None:
_LOGGER.warning("Unknown mode '%s'", vs_mode)
return ha_mode
def _get_vs_mode(ha_mode: str) -> str | None:
return HA_TO_VS_MODE_MAP.get(ha_mode)
class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity):
"""Representation of a VeSync humidifier."""
# The base VeSyncBaseEntity has _attr_has_entity_name and this is to follow the device name
_attr_name = None
_attr_max_humidity = MAX_HUMIDITY
_attr_min_humidity = MIN_HUMIDITY
_attr_supported_features = HumidifierEntityFeature.MODES
device: VeSyncHumidifierDevice
@property
def available_modes(self) -> list[str]:
"""Return the available mist modes."""
return [
ha_mode
for ha_mode in (_get_ha_mode(vs_mode) for vs_mode in self.device.mist_modes)
if ha_mode
]
@property
def target_humidity(self) -> int:
"""Return the humidity we try to reach."""
return self.device.config["auto_target_humidity"]
@property
def mode(self) -> str | None:
"""Get the current preset mode."""
return _get_ha_mode(self.device.details["mode"])
def set_humidity(self, humidity: int) -> None:
"""Set the target humidity of the device."""
if not self.device.set_humidity(humidity):
raise HomeAssistantError(
f"An error occurred while setting humidity {humidity}."
)
def set_mode(self, mode: str) -> None:
"""Set the mode of the device."""
if mode not in self.available_modes:
raise HomeAssistantError(
"{mode} is not one of the valid available modes: {self.available_modes}"
)
if not self.device.set_humidity_mode(_get_vs_mode(mode)):
raise HomeAssistantError(f"An error occurred while setting mode {mode}.")
def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
success = self.device.turn_on()
if not success:
raise HomeAssistantError("An error occurred while turning on.")
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
success = self.device.turn_off()
if not success:
raise HomeAssistantError("An error occurred while turning off.")
@property
def is_on(self) -> bool:
"""Return True if device is on."""
return self.device.device_status == "on"

View File

@ -7,9 +7,6 @@ from dataclasses import dataclass
import logging
from pyvesync.vesyncbasedevice import VeSyncBaseDevice
from pyvesync.vesyncfan import VeSyncAirBypass
from pyvesync.vesyncoutlet import VeSyncOutlet
from pyvesync.vesyncswitch import VeSyncSwitch
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -31,6 +28,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .common import is_humidifier
from .const import (
DEV_TYPE_TO_HA,
DOMAIN,
@ -49,14 +47,10 @@ _LOGGER = logging.getLogger(__name__)
class VeSyncSensorEntityDescription(SensorEntityDescription):
"""Describe VeSync sensor entity."""
value_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], StateType]
value_fn: Callable[[VeSyncBaseDevice], StateType]
exists_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], bool] = (
lambda _: True
)
update_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], None] = (
lambda _: None
)
exists_fn: Callable[[VeSyncBaseDevice], bool] = lambda _: True
update_fn: Callable[[VeSyncBaseDevice], None] = lambda _: None
def update_energy(device):
@ -186,6 +180,14 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = (
update_fn=update_energy,
exists_fn=lambda device: ha_dev_type(device) == "outlet",
),
VeSyncSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.details["humidity"],
exists_fn=is_humidifier,
),
)
@ -213,7 +215,7 @@ async def async_setup_entry(
@callback
def _setup_entities(
devices: list[VeSyncBaseDevice],
async_add_entities,
async_add_entities: AddEntitiesCallback,
coordinator: VeSyncDataCoordinator,
):
"""Check if device is online and add entity."""
@ -236,9 +238,9 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity):
def __init__(
self,
device: VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch,
device: VeSyncBaseDevice,
description: VeSyncSensorEntityDescription,
coordinator,
coordinator: VeSyncDataCoordinator,
) -> None:
"""Initialize the VeSync outlet device."""
super().__init__(device, coordinator)

View File

@ -16,7 +16,7 @@ ALL_DEVICE_NAMES: list[str] = [
]
DEVICE_FIXTURES: dict[str, list[tuple[str, str, str]]] = {
"Humidifier 200s": [
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")
("post", "/cloud/v2/deviceManaged/bypassV2", "humidifier-200s.json")
],
"Humidifier 600S": [
("post", "/cloud/v2/deviceManaged/bypassV2", "device-detail.json")

View File

@ -7,9 +7,10 @@ from unittest.mock import Mock, patch
import pytest
from pyvesync import VeSync
from pyvesync.vesyncbulb import VeSyncBulb
from pyvesync.vesyncfan import VeSyncAirBypass
from pyvesync.vesyncfan import VeSyncAirBypass, VeSyncHumid200300S
from pyvesync.vesyncoutlet import VeSyncOutlet
from pyvesync.vesyncswitch import VeSyncSwitch
import requests_mock
from homeassistant.components.vesync import DOMAIN
from homeassistant.config_entries import ConfigEntry
@ -17,6 +18,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .common import mock_multiple_device_responses
from tests.common import MockConfigEntry
@ -100,3 +103,29 @@ def dimmable_switch_fixture():
def outlet_fixture():
"""Create a mock VeSync outlet fixture."""
return Mock(VeSyncOutlet)
@pytest.fixture(name="humidifier")
def humidifier_fixture():
"""Create a mock VeSync humidifier fixture."""
return Mock(VeSyncHumid200300S)
@pytest.fixture(name="humidifier_config_entry")
async def humidifier_config_entry(
hass: HomeAssistant, requests_mock: requests_mock.Mocker, config
) -> MockConfigEntry:
"""Create a mock VeSync config entry for Humidifier 200s."""
entry = MockConfigEntry(
title="VeSync",
domain=DOMAIN,
data=config[DOMAIN],
)
entry.add_to_hass(hass)
device_name = "Humidifier 200s"
mock_multiple_device_responses(requests_mock, [device_name])
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@ -0,0 +1,26 @@
{
"code": 0,
"result": {
"result": {
"humidity": 35,
"mist_virtual_level": 6,
"mode": "manual",
"water_lacks": true,
"water_tank_lifted": true,
"automatic_stop_reach_target": true,
"night_light_brightness": 10,
"enabled": true,
"level": 1,
"display": true,
"display_forever": false,
"child_lock": false,
"night_light": "off",
"configuration": {
"auto_target_humidity": 40,
"display": true,
"automatic_stop": true
}
},
"code": 0
}
}

View File

@ -464,6 +464,36 @@
# ---
# name: test_fan_state[Humidifier 200s][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'200s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Classic200S',
'model_id': None,
'name': 'Humidifier 200s',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_fan_state[Humidifier 200s][entities]
@ -472,6 +502,36 @@
# ---
# name: test_fan_state[Humidifier 600S][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'600s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LUH-A602S-WUS',
'model_id': None,
'name': 'Humidifier 600S',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_fan_state[Humidifier 600S][entities]

View File

@ -335,6 +335,36 @@
# ---
# name: test_light_state[Humidifier 200s][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'200s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Classic200S',
'model_id': None,
'name': 'Humidifier 200s',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_light_state[Humidifier 200s][entities]
@ -343,6 +373,36 @@
# ---
# name: test_light_state[Humidifier 600S][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'600s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LUH-A602S-WUS',
'model_id': None,
'name': 'Humidifier 600S',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_light_state[Humidifier 600S][entities]

View File

@ -651,20 +651,178 @@
# ---
# name: test_sensor_state[Humidifier 200s][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'200s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Classic200S',
'model_id': None,
'name': 'Humidifier 200s',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_sensor_state[Humidifier 200s][entities]
list([
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.humidifier_200s_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'vesync',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '200s-humidifier-humidity',
'unit_of_measurement': '%',
}),
])
# ---
# name: test_sensor_state[Humidifier 200s][sensor.humidifier_200s_humidity]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Humidifier 200s Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.humidifier_200s_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '35',
})
# ---
# name: test_sensor_state[Humidifier 600S][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'600s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LUH-A602S-WUS',
'model_id': None,
'name': 'Humidifier 600S',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_sensor_state[Humidifier 600S][entities]
list([
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.humidifier_600s_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'original_icon': None,
'original_name': 'Humidity',
'platform': 'vesync',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '600s-humidifier-humidity',
'unit_of_measurement': '%',
}),
])
# ---
# name: test_sensor_state[Humidifier 600S][sensor.humidifier_600s_humidity]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'humidity',
'friendly_name': 'Humidifier 600S Humidity',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.humidifier_600s_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '35',
})
# ---
# name: test_sensor_state[Outlet][devices]
list([
DeviceRegistryEntrySnapshot({

View File

@ -229,6 +229,36 @@
# ---
# name: test_switch_state[Humidifier 200s][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'200s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'Classic200S',
'model_id': None,
'name': 'Humidifier 200s',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_switch_state[Humidifier 200s][entities]
@ -237,6 +267,36 @@
# ---
# name: test_switch_state[Humidifier 600S][devices]
list([
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'vesync',
'600s-humidifier',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'VeSync',
'model': 'LUH-A602S-WUS',
'model_id': None,
'name': 'Humidifier 600S',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
}),
])
# ---
# name: test_switch_state[Humidifier 600S][entities]

View File

@ -0,0 +1,201 @@
"""Tests for the humidifer module."""
from contextlib import nullcontext
from unittest.mock import patch
import pytest
from homeassistant.components.humidifier import (
ATTR_HUMIDITY,
ATTR_MODE,
DOMAIN as HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
SERVICE_SET_MODE,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from tests.common import MockConfigEntry
NoException = nullcontext()
async def test_humidifier_state(
hass: HomeAssistant, humidifier_config_entry: MockConfigEntry
) -> None:
"""Test the resulting setup state is as expected for the platform."""
humidifier_id = "humidifier.humidifier_200s"
expected_entities = [
humidifier_id,
"sensor.humidifier_200s_humidity",
]
assert humidifier_config_entry.state is ConfigEntryState.LOADED
for entity_id in expected_entities:
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
assert hass.states.get("sensor.humidifier_200s_humidity").state == "35"
state = hass.states.get(humidifier_id)
# ATTR_HUMIDITY represents the target_humidity which comes from configuration.auto_target_humidity node
assert state.attributes.get(ATTR_HUMIDITY) == 40
async def test_set_target_humidity_invalid(
hass: HomeAssistant,
humidifier_config_entry: MockConfigEntry,
) -> None:
"""Test handling of invalid value in set_humidify method."""
humidifier_entity_id = "humidifier.humidifier_200s"
# Setting value out of range results in ServiceValidationError and
# VeSyncHumid200300S.set_humidity does not get called.
with (
patch("pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity") as method_mock,
pytest.raises(ServiceValidationError),
):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: humidifier_entity_id, ATTR_HUMIDITY: 20},
blocking=True,
)
await hass.async_block_till_done()
method_mock.assert_not_called()
@pytest.mark.parametrize(
("api_response", "expectation"),
[(True, NoException), (False, pytest.raises(HomeAssistantError))],
)
async def test_set_target_humidity_VeSync(
hass: HomeAssistant,
humidifier_config_entry: MockConfigEntry,
api_response: bool,
expectation,
) -> None:
"""Test handling of return value from VeSyncHumid200300S.set_humidity."""
humidifier_entity_id = "humidifier.humidifier_200s"
# If VeSyncHumid200300S.set_humidity fails (returns False), then HomeAssistantError is raised
with (
expectation,
patch(
"pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity",
return_value=api_response,
) as method_mock,
):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_HUMIDITY,
{ATTR_ENTITY_ID: humidifier_entity_id, ATTR_HUMIDITY: 54},
blocking=True,
)
await hass.async_block_till_done()
method_mock.assert_called_once()
@pytest.mark.parametrize(
("turn_on", "api_response", "expectation"),
[
(False, False, pytest.raises(HomeAssistantError)),
(False, True, NoException),
(True, False, pytest.raises(HomeAssistantError)),
(True, True, NoException),
],
)
async def test_turn_on_off(
hass: HomeAssistant,
humidifier_config_entry: MockConfigEntry,
turn_on: bool,
api_response: bool,
expectation,
) -> None:
"""Test turn_on/off methods."""
humidifier_entity_id = "humidifier.humidifier_200s"
# turn_on/turn_off returns False indicating failure in which case humidifier.turn_on/turn_off
# raises HomeAssistantError.
with (
expectation,
patch(
f"pyvesync.vesyncfan.VeSyncHumid200300S.{"turn_on" if turn_on else "turn_off"}",
return_value=api_response,
) as method_mock,
):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_TURN_ON if turn_on else SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: humidifier_entity_id},
blocking=True,
)
await hass.async_block_till_done()
method_mock.assert_called_once()
async def test_set_mode_invalid(
hass: HomeAssistant,
humidifier_config_entry: MockConfigEntry,
) -> None:
"""Test handling of invalid value in set_mode method."""
humidifier_entity_id = "humidifier.humidifier_200s"
with patch(
"pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity_mode"
) as method_mock:
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_MODE,
{ATTR_ENTITY_ID: humidifier_entity_id, ATTR_MODE: "something_invalid"},
blocking=True,
)
await hass.async_block_till_done()
method_mock.assert_not_called()
@pytest.mark.parametrize(
("api_response", "expectation"),
[(True, NoException), (False, pytest.raises(HomeAssistantError))],
)
async def test_set_mode_VeSync(
hass: HomeAssistant,
humidifier_config_entry: MockConfigEntry,
api_response: bool,
expectation,
) -> None:
"""Test handling of value in set_mode method."""
humidifier_entity_id = "humidifier.humidifier_200s"
# If VeSyncHumid200300S.set_humidity_mode fails (returns False), then HomeAssistantError is raised
with (
expectation,
patch(
"pyvesync.vesyncfan.VeSyncHumid200300S.set_humidity_mode",
return_value=api_response,
) as method_mock,
):
await hass.services.async_call(
HUMIDIFIER_DOMAIN,
SERVICE_SET_MODE,
{ATTR_ENTITY_ID: humidifier_entity_id, ATTR_MODE: "auto"},
blocking=True,
)
await hass.async_block_till_done()
method_mock.assert_called_once()

View File

@ -49,6 +49,7 @@ async def test_async_setup_entry__no_devices(
assert setups_mock.call_args.args[0] == config_entry
assert setups_mock.call_args.args[1] == [
Platform.FAN,
Platform.HUMIDIFIER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
@ -77,6 +78,7 @@ async def test_async_setup_entry__loads_fans(
assert setups_mock.call_args.args[0] == config_entry
assert setups_mock.call_args.args[1] == [
Platform.FAN,
Platform.HUMIDIFIER,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,