From 83afd1280767a054e6668a2332ece9333a6c068d Mon Sep 17 00:00:00 2001 From: Charles Blonde Date: Sun, 6 Aug 2017 13:08:46 +0200 Subject: [PATCH] Add support to Dyson 360 Eye robot vacuum using new vacuum platform (#8852) * Add support to Dyson 360 Eye robot vacuum using new vacuum platform * Fix tests with Python 3.5 * Code review * Code review - v2 * Code review - v3 --- homeassistant/components/dyson.py | 24 ++- homeassistant/components/fan/dyson.py | 4 +- homeassistant/components/sensor/dyson.py | 4 +- homeassistant/components/vacuum/__init__.py | 3 - homeassistant/components/vacuum/dyson.py | 213 ++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fan/test_dyson.py | 9 +- tests/components/sensor/test_dyson.py | 8 +- tests/components/test_dyson.py | 35 +++- tests/components/vacuum/test_dyson.py | 189 +++++++++++++++++ 11 files changed, 468 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/vacuum/dyson.py create mode 100644 tests/components/vacuum/test_dyson.py diff --git a/homeassistant/components/dyson.py b/homeassistant/components/dyson.py index d1bb1364569..3989c0bbe3e 100644 --- a/homeassistant/components/dyson.py +++ b/homeassistant/components/dyson.py @@ -13,7 +13,7 @@ from homeassistant.helpers import discovery from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, \ CONF_DEVICES -REQUIREMENTS = ['libpurecoollink==0.4.1'] +REQUIREMENTS = ['libpurecoollink==0.4.2'] _LOGGER = logging.getLogger(__name__) @@ -69,14 +69,17 @@ def setup(hass, config): dyson_device = next((d for d in dyson_devices if d.serial == device["device_id"]), None) if dyson_device: - connected = dyson_device.connect(None, device["device_ip"], - timeout, retry) - if connected: - _LOGGER.info("Connected to device %s", dyson_device) - hass.data[DYSON_DEVICES].append(dyson_device) - else: - _LOGGER.warning("Unable to connect to device %s", - dyson_device) + try: + connected = dyson_device.connect(device["device_ip"]) + if connected: + _LOGGER.info("Connected to device %s", dyson_device) + hass.data[DYSON_DEVICES].append(dyson_device) + else: + _LOGGER.warning("Unable to connect to device %s", + dyson_device) + except OSError as ose: + _LOGGER.error("Unable to connect to device %s: %s", + str(dyson_device.network_device), str(ose)) else: _LOGGER.warning( "Unable to find device %s in Dyson account", @@ -86,7 +89,7 @@ def setup(hass, config): for device in dyson_devices: _LOGGER.info("Trying to connect to device %s with timeout=%i " "and retry=%i", device, timeout, retry) - connected = device.connect(None, None, timeout, retry) + connected = device.auto_connect(timeout, retry) if connected: _LOGGER.info("Connected to device %s", device) hass.data[DYSON_DEVICES].append(device) @@ -98,5 +101,6 @@ def setup(hass, config): _LOGGER.debug("Starting sensor/fan components") discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "fan", DOMAIN, {}, config) + discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) return True diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index f0ae102f1e6..0e0e3fdfaf3 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -36,7 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.data[DYSON_FAN_DEVICES] = [] # Get Dyson Devices from parent component - for device in hass.data[DYSON_DEVICES]: + from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink + for device in [d for d in hass.data[DYSON_DEVICES] if + isinstance(d, DysonPureCoolLink)]: dyson_entity = DysonPureCoolLinkDevice(hass, device) hass.data[DYSON_FAN_DEVICES].append(dyson_entity) diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index f8246d036a8..62c77bb768f 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -29,7 +29,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] unit = hass.config.units.temperature_unit # Get Dyson Devices from parent component - for device in hass.data[DYSON_DEVICES]: + from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink + for device in [d for d in hass.data[DYSON_DEVICES] if + isinstance(d, DysonPureCoolLink)]: devices.append(DysonFilterLifeSensor(hass, device)) devices.append(DysonDustSensor(hass, device)) devices.append(DysonHumiditySensor(hass, device)) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ea12435c05d..08cdd637379 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -168,9 +168,6 @@ def send_command(hass, command, params=None, entity_id=None): @asyncio.coroutine def async_setup(hass, config): """Set up the vacuum component.""" - if not config[DOMAIN]: - return False - component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS) diff --git a/homeassistant/components/vacuum/dyson.py b/homeassistant/components/vacuum/dyson.py new file mode 100644 index 00000000000..a784b161d1c --- /dev/null +++ b/homeassistant/components/vacuum/dyson.py @@ -0,0 +1,213 @@ +""" +Support for the Dyson 360 eye vacuum cleaner robot. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/vacuum.dyson/ +""" +import asyncio +import logging + +from homeassistant.components.dyson import DYSON_DEVICES +from homeassistant.components.vacuum import (SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_STATUS, SUPPORT_STOP, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + VacuumDevice) +from homeassistant.util.icon import icon_for_battery_level + +ATTR_FULL_CLEAN_TYPE = "full_clean_type" +ATTR_CLEAN_ID = "clean_id" +ATTR_POSITION = "position" + +DEPENDENCIES = ['dyson'] + +_LOGGER = logging.getLogger(__name__) + +DYSON_360_EYE_DEVICES = "dyson_360_eye_devices" + +ICON = "mdi:roomba" + +SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ + SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | SUPPORT_STATUS | \ + SUPPORT_BATTERY | SUPPORT_STOP + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Dyson 360 Eye robot vacuum platform.""" + _LOGGER.info("Creating new Dyson 360 Eye robot vacuum") + if DYSON_360_EYE_DEVICES not in hass.data: + hass.data[DYSON_360_EYE_DEVICES] = [] + + # Get Dyson Devices from parent component + from libpurecoollink.dyson_360_eye import Dyson360Eye + for device in [d for d in hass.data[DYSON_DEVICES] if + isinstance(d, Dyson360Eye)]: + dyson_entity = Dyson360EyeDevice(device) + hass.data[DYSON_360_EYE_DEVICES].append(dyson_entity) + + add_devices(hass.data[DYSON_360_EYE_DEVICES]) + return True + + +class Dyson360EyeDevice(VacuumDevice): + """Dyson 360 Eye robot vacuum device.""" + + def __init__(self, device): + """Dyson 360 Eye robot vacuum device.""" + _LOGGER.info("Creating device %s", device.name) + self._device = device + self._icon = ICON + + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.async_add_job( + self._device.add_message_listener, self.on_message) + + def on_message(self, message): + """Called when new messages received from the vacuum.""" + _LOGGER.debug("Message received for %s device: %s", self.name, message) + self.schedule_update_ha_state() + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def icon(self): + """Return the icon to use for device.""" + return self._icon + + @property + def status(self): + """Return the status of the vacuum cleaner.""" + from libpurecoollink.const import Dyson360EyeMode + dyson_labels = { + Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", + Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", + Dyson360EyeMode.FULL_CLEAN_PAUSED: "Paused", + Dyson360EyeMode.FULL_CLEAN_RUNNING: "Cleaning", + Dyson360EyeMode.FULL_CLEAN_ABORTED: "Returning home", + Dyson360EyeMode.FULL_CLEAN_INITIATED: "Start cleaning", + Dyson360EyeMode.FAULT_USER_RECOVERABLE: "Error - device blocked", + Dyson360EyeMode.FAULT_REPLACE_ON_DOCK: + "Error - Replace device on dock", + Dyson360EyeMode.FULL_CLEAN_FINISHED: "Finished", + Dyson360EyeMode.FULL_CLEAN_NEEDS_CHARGE: "Need charging" + } + return dyson_labels.get(self._device.state.state, + self._device.state.state) + + @property + def battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self._device.state.battery_level + + @property + def fan_speed(self): + """Return the fan speed of the vacuum cleaner.""" + from libpurecoollink.const import PowerMode + speed_labels = { + PowerMode.MAX: "Max", + PowerMode.QUIET: "Quiet" + } + return speed_labels[self._device.state.power_mode] + + @property + def fan_speed_list(self): + """Get the list of available fan speed steps of the vacuum cleaner.""" + return ["Quiet", "Max"] + + @property + def device_state_attributes(self): + """Return the specific state attributes of this vacuum cleaner.""" + return { + ATTR_POSITION: str(self._device.state.position) + } + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + from libpurecoollink.const import Dyson360EyeMode + return self._device.state.state in [ + Dyson360EyeMode.FULL_CLEAN_INITIATED, + Dyson360EyeMode.FULL_CLEAN_ABORTED, + Dyson360EyeMode.FULL_CLEAN_RUNNING + ] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return True + + @property + def supported_features(self): + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_DYSON + + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + from libpurecoollink.const import Dyson360EyeMode + charging = self._device.state.state in [ + Dyson360EyeMode.INACTIVE_CHARGING] + return icon_for_battery_level( + battery_level=self.battery_level, charging=charging) + + def turn_on(self, **kwargs): + """Turn the vacuum on.""" + _LOGGER.debug("Turn on device %s", self.name) + from libpurecoollink.const import Dyson360EyeMode + if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: + self._device.resume() + else: + self._device.start() + + def turn_off(self, **kwargs): + """Turn the vacuum off and return to home.""" + _LOGGER.debug("Turn off device %s", self.name) + self._device.pause() + + def stop(self, **kwargs): + """Stop the vacuum cleaner.""" + _LOGGER.debug("Stop device %s", self.name) + self._device.pause() + + def set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) + from libpurecoollink.const import PowerMode + power_modes = { + "Quiet": PowerMode.QUIET, + "Max": PowerMode.MAX + } + self._device.set_power_mode(power_modes[fan_speed]) + + def start_pause(self, **kwargs): + """Start, pause or resume the cleaning task.""" + from libpurecoollink.const import Dyson360EyeMode + if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: + _LOGGER.debug("Resume device %s", self.name) + self._device.resume() + elif self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGED, + Dyson360EyeMode.INACTIVE_CHARGING]: + _LOGGER.debug("Start device %s", self.name) + self._device.start() + else: + _LOGGER.debug("Pause device %s", self.name) + self._device.pause() + + def return_to_base(self, **kwargs): + """Set the vacuum cleaner to return to the dock.""" + _LOGGER.debug("Return to base device %s", self.name) + self._device.abort() diff --git a/requirements_all.txt b/requirements_all.txt index 090ea842b80..da69840869f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ knxip==0.5 libnacl==1.5.2 # homeassistant.components.dyson -libpurecoollink==0.4.1 +libpurecoollink==0.4.2 # homeassistant.components.device_tracker.mikrotik librouteros==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7f93d0e9b1..3577584cfc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ holidays==0.8.1 influxdb==3.0.0 # homeassistant.components.dyson -libpurecoollink==0.4.1 +libpurecoollink==0.4.2 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 diff --git a/tests/components/fan/test_dyson.py b/tests/components/fan/test_dyson.py index 3573b9c9f48..49338e123e3 100644 --- a/tests/components/fan/test_dyson.py +++ b/tests/components/fan/test_dyson.py @@ -7,6 +7,7 @@ from homeassistant.components.fan import dyson from tests.common import get_test_home_assistant from libpurecoollink.const import FanSpeed, FanMode, NightMode, Oscillation from libpurecoollink.dyson_pure_state import DysonPureCoolState +from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink class MockDysonState(DysonPureCoolState): @@ -49,7 +50,7 @@ def _get_device_auto(): def _get_device_on(): """Return a valid state on.""" - device = mock.Mock() + device = mock.Mock(spec=DysonPureCoolLink) device.name = "Device_name" device.state = mock.Mock() device.state.fan_mode = "FAN" @@ -84,8 +85,10 @@ class DysonTest(unittest.TestCase): assert len(devices) == 1 assert devices[0].name == "Device_name" - device = _get_device_on() - self.hass.data[dyson.DYSON_DEVICES] = [device] + device_fan = _get_device_on() + device_non_fan = _get_device_off() + + self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] dyson.setup_platform(self.hass, None, _add_device) def test_dyson_set_speed(self): diff --git a/tests/components/sensor/test_dyson.py b/tests/components/sensor/test_dyson.py index a4a69b700b3..dcbafcae6e3 100644 --- a/tests/components/sensor/test_dyson.py +++ b/tests/components/sensor/test_dyson.py @@ -6,11 +6,12 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, \ STATE_OFF from homeassistant.components.sensor import dyson from tests.common import get_test_home_assistant +from libpurecoollink.dyson_pure_cool_link import DysonPureCoolLink def _get_device_without_state(): """Return a valid device provide by Dyson web services.""" - device = mock.Mock() + device = mock.Mock(spec=DysonPureCoolLink) device.name = "Device_name" device.state = None device.environmental_state = None @@ -75,8 +76,9 @@ class DysonTest(unittest.TestCase): assert devices[3].name == "Device_name temperature" assert devices[4].name == "Device_name air quality" - device = _get_device_without_state() - self.hass.data[dyson.DYSON_DEVICES] = [device] + device_fan = _get_device_without_state() + device_non_fan = _get_with_state() + self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] dyson.setup_platform(self.hass, None, _add_device) def test_dyson_filter_life_sensor(self): diff --git a/tests/components/test_dyson.py b/tests/components/test_dyson.py index fce88fefc2c..38f3e60dcf4 100644 --- a/tests/components/test_dyson.py +++ b/tests/components/test_dyson.py @@ -11,6 +11,7 @@ def _get_dyson_account_device_available(): device = mock.Mock() device.serial = "XX-XXXXX-XX" device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) return device @@ -19,6 +20,15 @@ def _get_dyson_account_device_not_available(): device = mock.Mock() device.serial = "XX-XXXXX-XX" device.connect = mock.Mock(return_value=False) + device.auto_connect = mock.Mock(return_value=False) + return device + + +def _get_dyson_account_device_error(): + """Return an invalid device raising OSError while connecting.""" + device = mock.Mock() + device.serial = "XX-XXXXX-XX" + device.connect = mock.Mock(side_effect=OSError("Network error")) return device @@ -77,7 +87,7 @@ class DysonTest(unittest.TestCase): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) - self.assertEqual(mocked_discovery.call_count, 2) + self.assertEqual(mocked_discovery.call_count, 3) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) @@ -100,6 +110,27 @@ class DysonTest(unittest.TestCase): self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + @mock.patch('libpurecoollink.dyson.DysonAccount.devices', + return_value=[_get_dyson_account_device_error()]) + @mock.patch('libpurecoollink.dyson.DysonAccount.login', return_value=True) + def test_dyson_custom_conf_device_error(self, mocked_login, + mocked_devices): + """Test device connection with device raising an exception.""" + dyson.setup(self.hass, {dyson.DOMAIN: { + dyson.CONF_USERNAME: "email", + dyson.CONF_PASSWORD: "password", + dyson.CONF_LANGUAGE: "FR", + dyson.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }}) + self.assertEqual(mocked_login.call_count, 1) + self.assertEqual(mocked_devices.call_count, 1) + self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 0) + @mock.patch('homeassistant.helpers.discovery.load_platform') @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_available()]) @@ -141,7 +172,7 @@ class DysonTest(unittest.TestCase): self.assertEqual(mocked_login.call_count, 1) self.assertEqual(mocked_devices.call_count, 1) self.assertEqual(len(self.hass.data[dyson.DYSON_DEVICES]), 1) - self.assertEqual(mocked_discovery.call_count, 2) + self.assertEqual(mocked_discovery.call_count, 3) @mock.patch('libpurecoollink.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) diff --git a/tests/components/vacuum/test_dyson.py b/tests/components/vacuum/test_dyson.py new file mode 100644 index 00000000000..186a2271a73 --- /dev/null +++ b/tests/components/vacuum/test_dyson.py @@ -0,0 +1,189 @@ +"""Test the Dyson 360 eye robot vacuum component.""" +import unittest +from unittest import mock + +from libpurecoollink.dyson_360_eye import Dyson360Eye +from libpurecoollink.const import Dyson360EyeMode, PowerMode + +from homeassistant.components.vacuum import dyson +from homeassistant.components.vacuum.dyson import Dyson360EyeDevice +from tests.common import get_test_home_assistant + + +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() + device.state.state = Dyson360EyeMode.FULL_CLEAN_RUNNING + device.state.battery_level = 85 + device.state.power_mode = PowerMode.QUIET + device.state.position = (0, 0) + 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() + device.state.state = Dyson360EyeMode.INACTIVE_CHARGING + device.state.battery_level = 40 + device.state.power_mode = PowerMode.QUIET + device.state.position = (0, 0) + return device + + +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 + + +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 + + +class DysonTest(unittest.TestCase): + """Dyson 360 eye robot vacuum component test class.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """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_devices = mock.MagicMock() + dyson.setup_platform(self.hass, {}, add_devices) + add_devices.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()) + self.assertTrue(component.schedule_update_ha_state.called) + + def test_should_poll(self): + """Test polling is disable.""" + device = _get_vacuum_device_cleaning() + component = Dyson360EyeDevice(device) + self.assertFalse(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) + self.assertEqual(component.name, "Device_Vacuum") + self.assertTrue(component.is_on) + self.assertEqual(component.icon, "mdi:roomba") + self.assertEqual(component.status, "Cleaning") + self.assertEqual(component2.status, "Unknown") + self.assertEqual(component.battery_level, 85) + self.assertEqual(component.fan_speed, "Quiet") + self.assertEqual(component.fan_speed_list, ["Quiet", "Max"]) + self.assertEqual(component.device_state_attributes['position'], + '(0, 0)') + self.assertTrue(component.available) + self.assertEqual(component.supported_features, 255) + self.assertEqual(component.battery_icon, "mdi:battery-80") + self.assertEqual(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() + self.assertTrue(device1.start.called) + + device2 = _get_vacuum_device_pause() + component2 = Dyson360EyeDevice(device2) + component2.turn_on() + self.assertTrue(device2.resume.called) + + def test_turn_off(self): + """Test turn off vacuum.""" + device1 = _get_vacuum_device_cleaning() + component1 = Dyson360EyeDevice(device1) + component1.turn_off() + self.assertTrue(device1.pause.called) + + def test_stop(self): + """Test stop vacuum.""" + device1 = _get_vacuum_device_cleaning() + component1 = Dyson360EyeDevice(device1) + component1.stop() + self.assertTrue(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() + self.assertTrue(device1.start.called) + + device2 = _get_vacuum_device_pause() + component2 = Dyson360EyeDevice(device2) + component2.start_pause() + self.assertTrue(device2.resume.called) + + device3 = _get_vacuum_device_cleaning() + component3 = Dyson360EyeDevice(device3) + component3.start_pause() + self.assertTrue(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() + self.assertTrue(device.abort.called)