mirror of
https://github.com/home-assistant/core.git
synced 2026-08-08 14:25:42 +01:00
357 lines
10 KiB
Python
357 lines
10 KiB
Python
"""Tests for the fan module."""
|
|
|
|
from contextlib import nullcontext
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components.fan import (
|
|
ATTR_PERCENTAGE,
|
|
ATTR_PRESET_MODE,
|
|
ATTR_PRESET_MODES,
|
|
DOMAIN as FAN_DOMAIN,
|
|
)
|
|
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
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
|
from .common import (
|
|
ALL_DEVICE_NAMES,
|
|
ENTITY_FAN,
|
|
ENTITY_PEDESTAL_FAN,
|
|
mock_devices_response,
|
|
)
|
|
|
|
from tests.common import MockConfigEntry
|
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
|
|
|
NoException = nullcontext()
|
|
|
|
|
|
@pytest.mark.parametrize("device_name", ALL_DEVICE_NAMES)
|
|
async def test_fan_state(
|
|
hass: HomeAssistant,
|
|
snapshot: SnapshotAssertion,
|
|
config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
device_name: str,
|
|
) -> None:
|
|
"""Test the resulting setup state is as expected for the platform."""
|
|
|
|
# Configure the API devices call for device_name
|
|
mock_devices_response(aioclient_mock, device_name)
|
|
|
|
# setup platform - only including the named device
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Check device registry
|
|
devices = dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)
|
|
assert devices == snapshot(name="devices")
|
|
|
|
# Check entity registry
|
|
entities = [
|
|
entity
|
|
for entity in er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
if entity.domain == FAN_DOMAIN
|
|
]
|
|
assert entities == snapshot(name="entities")
|
|
|
|
# Check states
|
|
for entity in entities:
|
|
assert hass.states.get(entity.entity_id) == snapshot(name=entity.entity_id)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "command"),
|
|
[
|
|
(SERVICE_TURN_ON, "pyvesync.devices.vesyncfan.VeSyncTowerFan.turn_on"),
|
|
(SERVICE_TURN_OFF, "pyvesync.devices.vesyncfan.VeSyncTowerFan.turn_off"),
|
|
],
|
|
)
|
|
async def test_turn_on_off_success(
|
|
hass: HomeAssistant,
|
|
fan_config_entry: MockConfigEntry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
action: str,
|
|
command: str,
|
|
) -> None:
|
|
"""Test turn_on and turn_off method."""
|
|
|
|
mock_devices_response(aioclient_mock, "SmartTowerFan")
|
|
|
|
with (
|
|
patch(command, new_callable=AsyncMock, return_value=True) as method_mock,
|
|
):
|
|
with patch(
|
|
"homeassistant.components.vesync.fan.VeSyncFanHA.async_write_ha_state"
|
|
) as update_mock:
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
action,
|
|
{ATTR_ENTITY_ID: ENTITY_FAN},
|
|
blocking=True,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
method_mock.assert_called_once()
|
|
update_mock.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "command"),
|
|
[
|
|
(
|
|
SERVICE_TURN_ON,
|
|
"pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice.turn_on",
|
|
),
|
|
(
|
|
SERVICE_TURN_OFF,
|
|
"pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice.turn_off",
|
|
),
|
|
],
|
|
)
|
|
async def test_turn_on_off_raises_error(
|
|
hass: HomeAssistant,
|
|
fan_config_entry: MockConfigEntry,
|
|
action: str,
|
|
command: str,
|
|
) -> None:
|
|
"""Test turn_on and turn_off raises errors when fails."""
|
|
|
|
# returns False indicating failure in which case raises HomeAssistantError.
|
|
with (
|
|
patch(
|
|
command,
|
|
return_value=False,
|
|
) as method_mock,
|
|
pytest.raises(HomeAssistantError),
|
|
):
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
action,
|
|
{ATTR_ENTITY_ID: ENTITY_FAN},
|
|
blocking=True,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
method_mock.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("api_response", "expectation"),
|
|
[(True, NoException), (False, pytest.raises(HomeAssistantError))],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("preset_mode", "patch_target"),
|
|
[
|
|
("normal", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_normal_mode"),
|
|
(
|
|
"sleep",
|
|
"pyvesync.devices.vesyncfan.VeSyncTowerFan.set_sleep_mode",
|
|
),
|
|
("turbo", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_turbo_mode"),
|
|
("auto", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_auto_mode"),
|
|
],
|
|
)
|
|
async def test_set_preset_mode(
|
|
hass: HomeAssistant,
|
|
fan_config_entry: MockConfigEntry,
|
|
api_response: bool,
|
|
expectation,
|
|
preset_mode: str,
|
|
patch_target: str,
|
|
) -> None:
|
|
"""Test set_preset_mode method via turn on for test coverage."""
|
|
|
|
# If VeSyncTowerFan.mode fails (returns False), then HomeAssistantError is raised
|
|
with (
|
|
expectation,
|
|
patch(
|
|
patch_target,
|
|
return_value=api_response,
|
|
) as method_mock,
|
|
):
|
|
with patch(
|
|
"homeassistant.components.vesync.fan.VeSyncFanHA.async_write_ha_state"
|
|
) as update_mock:
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
SERVICE_TURN_ON,
|
|
{ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PRESET_MODE: preset_mode},
|
|
blocking=True,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
method_mock.assert_called_once()
|
|
update_mock.assert_called_once()
|
|
|
|
|
|
async def test_out_of_range_fan_level(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test that an out-of-range fan level produces an unknown percentage."""
|
|
|
|
mock_devices_response(
|
|
aioclient_mock, "CoreBreeze 432S", details_override={"fanSpeedLevel": -1}
|
|
)
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(ENTITY_PEDESTAL_FAN)
|
|
assert state is not None
|
|
assert state.state != STATE_UNAVAILABLE
|
|
assert state.attributes[ATTR_PERCENTAGE] is None
|
|
|
|
|
|
async def test_set_preset_mode_eco(
|
|
hass: HomeAssistant,
|
|
config_entry: MockConfigEntry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test the eco preset is exposed, reflected, and sets the mode via set_mode."""
|
|
mock_devices_response(
|
|
aioclient_mock, "CoreBreeze 432S", details_override={"workMode": "eco"}
|
|
)
|
|
|
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
state = hass.states.get(ENTITY_PEDESTAL_FAN)
|
|
assert state is not None
|
|
assert "eco" in state.attributes[ATTR_PRESET_MODES]
|
|
assert state.attributes[ATTR_PRESET_MODE] == "eco"
|
|
|
|
with (
|
|
patch(
|
|
"pyvesync.devices.vesyncfan.VeSyncPedestalFan.set_mode",
|
|
new_callable=AsyncMock,
|
|
return_value=True,
|
|
) as method_mock,
|
|
patch(
|
|
"homeassistant.components.vesync.fan.VeSyncFanHA.async_write_ha_state"
|
|
) as update_mock,
|
|
):
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
SERVICE_TURN_ON,
|
|
{ATTR_ENTITY_ID: ENTITY_PEDESTAL_FAN, ATTR_PRESET_MODE: "eco"},
|
|
blocking=True,
|
|
)
|
|
await hass.async_block_till_done()
|
|
|
|
method_mock.assert_awaited_once_with("eco")
|
|
update_mock.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "api_response", "expectation"),
|
|
[
|
|
("true", True, NoException),
|
|
("false", True, NoException),
|
|
("true", False, pytest.raises(HomeAssistantError)),
|
|
],
|
|
)
|
|
async def test_pedestal_fan_oscillation(
|
|
hass: HomeAssistant,
|
|
pedestal_fan_config_entry: MockConfigEntry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
action: str,
|
|
api_response: bool,
|
|
expectation,
|
|
) -> None:
|
|
"""Test oscillation on and off for pedestal fans.
|
|
|
|
Pedestal fans (e.g. CoreBreeze 432S / LPF-R432S) expose vertical and
|
|
horizontal oscillation as separate toggles. The HA oscillate switch
|
|
should control both axes together.
|
|
"""
|
|
|
|
with (
|
|
expectation,
|
|
patch(
|
|
"pyvesync.devices.vesyncfan.VeSyncPedestalFan.toggle_vertical_oscillation",
|
|
new_callable=AsyncMock,
|
|
return_value=api_response,
|
|
) as vertical_mock,
|
|
patch(
|
|
"pyvesync.devices.vesyncfan.VeSyncPedestalFan."
|
|
"toggle_horizontal_oscillation",
|
|
new_callable=AsyncMock,
|
|
return_value=api_response,
|
|
) as horizontal_mock,
|
|
):
|
|
with patch(
|
|
"homeassistant.components.vesync.fan.VeSyncFanHA.async_write_ha_state"
|
|
) as update_mock:
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
"oscillate",
|
|
{ATTR_ENTITY_ID: ENTITY_PEDESTAL_FAN, "oscillating": action},
|
|
blocking=True,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
vertical_mock.assert_called_once()
|
|
horizontal_mock.assert_called_once()
|
|
update_mock.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("action", "command"),
|
|
[
|
|
("true", "pyvesync.devices.vesyncfan.VeSyncTowerFan.toggle_oscillation"),
|
|
("false", "pyvesync.devices.vesyncfan.VeSyncTowerFan.toggle_oscillation"),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
("api_response", "expectation"),
|
|
[(True, NoException), (False, pytest.raises(HomeAssistantError))],
|
|
)
|
|
async def test_oscillation_success(
|
|
hass: HomeAssistant,
|
|
fan_config_entry: MockConfigEntry,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
action: str,
|
|
command: str,
|
|
api_response: bool,
|
|
expectation,
|
|
) -> None:
|
|
"""Test oscillation on and off."""
|
|
|
|
mock_devices_response(aioclient_mock, "SmartTowerFan")
|
|
|
|
with (
|
|
expectation,
|
|
patch(
|
|
command, new_callable=AsyncMock, return_value=api_response
|
|
) as method_mock,
|
|
):
|
|
with patch(
|
|
"homeassistant.components.vesync.fan.VeSyncFanHA.async_write_ha_state"
|
|
) as update_mock:
|
|
await hass.services.async_call(
|
|
FAN_DOMAIN,
|
|
"oscillate",
|
|
{ATTR_ENTITY_ID: ENTITY_FAN, "oscillating": action},
|
|
blocking=True,
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
method_mock.assert_called_once()
|
|
update_mock.assert_called_once()
|