diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py index f1dabe5203d..be6376b9c29 100644 --- a/tests/components/dyson/common.py +++ b/tests/components/dyson/common.py @@ -1,22 +1,27 @@ """Common utils for Dyson tests.""" +from typing import Optional, Type from unittest import mock +from unittest.mock import MagicMock +from libpurecool.dyson_device import DysonDevice from libpurecool.dyson_pure_cool import FanSpeed +from homeassistant.core import HomeAssistant -def load_mock_device(device): +SERIAL = "XX-XXXXX-XX" +NAME = "Temp Name" +ENTITY_NAME = "temp_name" + +BASE_PATH = "homeassistant.components.dyson" + + +def load_mock_device(device: DysonDevice) -> None: """Load the mock with default values so it doesn't throw errors.""" - device.serial = "XX-XXXXX-XX" - device.name = "Temp Name" + device.serial = SERIAL + device.name = NAME device.connect = mock.Mock(return_value=True) device.auto_connect = mock.Mock(return_value=True) - device.environmental_state.particulate_matter_25 = "0000" - device.environmental_state.particulate_matter_10 = "0000" - device.environmental_state.nitrogen_dioxide = "0000" - device.environmental_state.volatil_organic_compounds = "0000" - device.environmental_state.volatile_organic_compounds = "0000" - device.environmental_state.temperature = 250 device.state.hepa_filter_state = 0 device.state.carbon_filter_state = 0 device.state.speed = FanSpeed.FAN_SPEED_1.value @@ -24,3 +29,27 @@ def load_mock_device(device): device.state.oscillation_angle_high = "000" device.state.filter_life = "000" device.state.heat_target = 200 + if hasattr(device, "environmental_state"): + device.environmental_state.particulate_matter_25 = "0000" + device.environmental_state.particulate_matter_10 = "0000" + device.environmental_state.nitrogen_dioxide = "0000" + device.environmental_state.volatil_organic_compounds = "0000" + device.environmental_state.volatile_organic_compounds = "0000" + device.environmental_state.temperature = 250 + + +def get_basic_device(spec: Type[DysonDevice]) -> DysonDevice: + """Return a basic device with common fields filled out.""" + device = MagicMock(spec=spec) + load_mock_device(device) + return device + + +async def async_update_device( + hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None +) -> None: + """Update the device using callback function.""" + callback = device.add_message_listener.call_args[0][0] + message = MagicMock(spec=state_type) + await hass.async_add_executor_job(callback, message) + await hass.async_block_till_done() diff --git a/tests/components/dyson/conftest.py b/tests/components/dyson/conftest.py new file mode 100644 index 00000000000..9edef6bae0c --- /dev/null +++ b/tests/components/dyson/conftest.py @@ -0,0 +1,46 @@ +"""Configure pytest for Dyson tests.""" +from unittest.mock import patch + +from libpurecool.dyson_device import DysonDevice +import pytest + +from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .common import SERIAL + +from tests.common import async_setup_component + +BASE_PATH = "homeassistant.components.dyson" + + +@pytest.fixture +async def device(hass: HomeAssistant, request) -> DysonDevice: + """Fixture to provide Dyson 360 Eye device.""" + device = request.module.get_device() + platform = request.module.PLATFORM_DOMAIN + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=[device] + ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [platform]): + # DYSON_PLATFORMS is patched so that only the platform being tested is set up + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_USERNAME: "user@example.com", + CONF_PASSWORD: "password", + CONF_LANGUAGE: "US", + CONF_DEVICES: [ + { + "device_id": SERIAL, + "device_ip": "0.0.0.0", + } + ], + } + }, + ) + await hass.async_block_till_done() + + return device diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py index 0ff19665807..c43c9d80e66 100644 --- a/tests/components/dyson/test_vacuum.py +++ b/tests/components/dyson/test_vacuum.py @@ -1,29 +1,43 @@ """Test the Dyson 360 eye robot vacuum component.""" -import unittest -from unittest import mock +from unittest.mock import MagicMock from libpurecool.const import Dyson360EyeMode, PowerMode from libpurecool.dyson_360_eye import Dyson360Eye +import pytest -from homeassistant.components.dyson import vacuum as dyson -from homeassistant.components.dyson.vacuum import Dyson360EyeDevice +from homeassistant.components.dyson.vacuum import ATTR_POSITION, SUPPORT_DYSON +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + ATTR_FAN_SPEED_LIST, + ATTR_STATUS, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START_PAUSE, + SERVICE_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry -from tests.common import get_test_home_assistant +from .common import ENTITY_NAME, NAME, SERIAL, async_update_device, get_basic_device + +ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" -def _get_non_vacuum_device(): - """Return a non vacuum device.""" - device = mock.Mock() - device.name = "Device_Fan" - device.state = None - return device - - -def _get_vacuum_device_cleaning(): - """Return a vacuum device running.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() +@callback +def get_device() -> Dyson360Eye: + """Return a Dyson 360 Eye device.""" + device = get_basic_device(Dyson360Eye) + device.state = MagicMock() device.state.state = Dyson360EyeMode.FULL_CLEAN_RUNNING device.state.battery_level = 85 device.state.power_mode = PowerMode.QUIET @@ -31,159 +45,74 @@ def _get_vacuum_device_cleaning(): return device -def _get_vacuum_device_charging(): - """Return a vacuum device charging.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() +async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None: + """Test the state of the vacuum.""" + er = await entity_registry.async_get_registry(hass) + assert er.async_get(ENTITY_ID).unique_id == SERIAL + + state = hass.states.get(ENTITY_ID) + assert state.name == NAME + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_STATUS] == "Cleaning" + assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_DYSON + assert attributes[ATTR_BATTERY_LEVEL] == 85 + assert attributes[ATTR_POSITION] == "(0, 0)" + assert attributes[ATTR_FAN_SPEED] == "Quiet" + assert attributes[ATTR_FAN_SPEED_LIST] == ["Quiet", "Max"] + device.state.state = Dyson360EyeMode.INACTIVE_CHARGING - device.state.battery_level = 40 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device + device.state.power_mode = PowerMode.MAX + await async_update_device(hass, device) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert state.attributes[ATTR_STATUS] == "Stopped - Charging" + assert state.attributes[ATTR_FAN_SPEED] == "Max" - -def _get_vacuum_device_pause(): - """Return a vacuum device in pause.""" - device = mock.MagicMock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() device.state.state = Dyson360EyeMode.FULL_CLEAN_PAUSED - device.state.battery_level = 40 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device + await async_update_device(hass, device) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert state.attributes[ATTR_STATUS] == "Paused" -def _get_vacuum_device_unknown_state(): - """Return a vacuum device with unknown state.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() - device.state.state = "Unknown" - return device +@pytest.mark.parametrize( + "service,command,state", + [ + (SERVICE_TURN_ON, "start", Dyson360EyeMode.INACTIVE_CHARGED), + (SERVICE_TURN_ON, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), + (SERVICE_TURN_OFF, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_STOP, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "start", Dyson360EyeMode.INACTIVE_CHARGED), + (SERVICE_START_PAUSE, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), + (SERVICE_RETURN_TO_BASE, "abort", Dyson360EyeMode.FULL_CLEAN_PAUSED), + ], +) +async def test_commands( + hass: HomeAssistant, device: Dyson360Eye, service: str, command: str, state: str +) -> None: + """Test sending commands to the vacuum.""" + device.state.state = state + await async_update_device(hass, device) + await hass.services.async_call( + PLATFORM_DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + getattr(device, command).assert_called_once_with() -class DysonTest(unittest.TestCase): - """Dyson 360 eye robot vacuum component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component_with_no_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, {}, add_entities) - add_entities.assert_called_with([]) - - def test_setup_component(self): - """Test setup component with devices.""" - - def _add_device(devices): - assert len(devices) == 1 - assert devices[0].name == "Device_Vacuum" - - device_vacuum = _get_vacuum_device_cleaning() - device_non_vacuum = _get_non_vacuum_device() - self.hass.data[dyson.DYSON_DEVICES] = [device_vacuum, device_non_vacuum] - dyson.setup_platform(self.hass, {}, _add_device) - - def test_on_message(self): - """Test when message is received.""" - device = _get_vacuum_device_cleaning() - component = Dyson360EyeDevice(device) - component.entity_id = "entity_id" - component.schedule_update_ha_state = mock.Mock() - component.on_message(mock.Mock()) - assert component.schedule_update_ha_state.called - - def test_should_poll(self): - """Test polling is disable.""" - device = _get_vacuum_device_cleaning() - component = Dyson360EyeDevice(device) - assert not component.should_poll - - def test_properties(self): - """Test component properties.""" - device1 = _get_vacuum_device_cleaning() - device2 = _get_vacuum_device_unknown_state() - device3 = _get_vacuum_device_charging() - component = Dyson360EyeDevice(device1) - component2 = Dyson360EyeDevice(device2) - component3 = Dyson360EyeDevice(device3) - assert component.name == "Device_Vacuum" - assert component.is_on - assert component.status == "Cleaning" - assert component2.status == "Unknown" - assert component.battery_level == 85 - assert component.fan_speed == "Quiet" - assert component.fan_speed_list == ["Quiet", "Max"] - assert component.device_state_attributes["position"] == "(0, 0)" - assert component.available - assert component.supported_features == 255 - assert component.battery_icon == "mdi:battery-80" - assert component3.battery_icon == "mdi:battery-charging-40" - - def test_turn_on(self): - """Test turn on vacuum.""" - device1 = _get_vacuum_device_charging() - component1 = Dyson360EyeDevice(device1) - component1.turn_on() - assert device1.start.called - - device2 = _get_vacuum_device_pause() - component2 = Dyson360EyeDevice(device2) - component2.turn_on() - assert device2.resume.called - - def test_turn_off(self): - """Test turn off vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.turn_off() - assert device1.pause.called - - def test_stop(self): - """Test stop vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.stop() - assert device1.pause.called - - def test_set_fan_speed(self): - """Test set fan speed vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.set_fan_speed("Max") - device1.set_power_mode.assert_called_with(PowerMode.MAX) - - def test_start_pause(self): - """Test start/pause.""" - device1 = _get_vacuum_device_charging() - component1 = Dyson360EyeDevice(device1) - component1.start_pause() - assert device1.start.called - - device2 = _get_vacuum_device_pause() - component2 = Dyson360EyeDevice(device2) - component2.start_pause() - assert device2.resume.called - - device3 = _get_vacuum_device_cleaning() - component3 = Dyson360EyeDevice(device3) - component3.start_pause() - assert device3.pause.called - - def test_return_to_base(self): - """Test return to base.""" - device = _get_vacuum_device_pause() - component = Dyson360EyeDevice(device) - component.return_to_base() - assert device.abort.called +async def test_set_fan_speed(hass: HomeAssistant, device: Dyson360Eye): + """Test setting fan speed of the vacuum.""" + fan_speed_map = { + "Max": PowerMode.MAX, + "Quiet": PowerMode.QUIET, + } + for service_speed, command_speed in fan_speed_map.items(): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: service_speed}, + blocking=True, + ) + device.set_power_mode.assert_called_with(command_speed)