From a221b10694a5b28270c28937c9aca6878253e899 Mon Sep 17 00:00:00 2001
From: Eugenio Panadero <eugenio.panadero@gmail.com>
Date: Sat, 5 Aug 2017 21:45:59 +0200
Subject: [PATCH] Update xiaomi vacuum tests and include in coverage (#8845)

* Fix tests for Demo vacuum platform (and increase coverage)

* increase coverage of xiaomi vacuum tests and include in coverage

Also little fixes

* remove print statement
---
 .coveragerc                                 |   6 +-
 homeassistant/components/vacuum/__init__.py |  15 +-
 homeassistant/components/vacuum/demo.py     |  17 +-
 homeassistant/components/vacuum/xiaomi.py   |  30 ++--
 tests/components/vacuum/test_demo.py        |  63 ++++++-
 tests/components/vacuum/test_xiaomi.py      | 186 ++++++++++++++++----
 6 files changed, 240 insertions(+), 77 deletions(-)

diff --git a/.coveragerc b/.coveragerc
index 67b4d8be258..1ab8e6c1346 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -197,7 +197,11 @@ omit =
     homeassistant/components/*/wink.py
 
     homeassistant/components/xiaomi.py
-    homeassistant/components/*/xiaomi.py
+    homeassistant/components/binary_sensor/xiaomi.py
+    homeassistant/components/cover/xiaomi.py
+    homeassistant/components/light/xiaomi.py
+    homeassistant/components/sensor/xiaomi.py
+    homeassistant/components/switch/xiaomi.py
 
     homeassistant/components/zabbix.py
     homeassistant/components/*/zabbix.py
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index 078be8e259f..ea12435c05d 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -184,9 +184,6 @@ def async_setup(hass, config):
     def async_handle_vacuum_service(service):
         """Map services to methods on VacuumDevice."""
         method = SERVICE_TO_METHOD.get(service.service)
-        if not method:
-            return
-
         target_vacuums = component.async_extract_from_service(service)
         params = service.data.copy()
         params.pop(ATTR_ENTITY_ID, None)
@@ -223,17 +220,17 @@ class VacuumDevice(ToggleEntity):
     @property
     def supported_features(self):
         """Flag vacuum cleaner features that are supported."""
-        return 0
+        raise NotImplementedError()
 
     @property
     def status(self):
         """Return the status of the vacuum cleaner."""
-        return None
+        raise NotImplementedError()
 
     @property
     def battery_level(self):
         """Return the battery level of the vacuum cleaner."""
-        return None
+        raise NotImplementedError()
 
     @property
     def battery_icon(self):
@@ -247,12 +244,12 @@ class VacuumDevice(ToggleEntity):
     @property
     def fan_speed(self):
         """Return the fan speed of the vacuum cleaner."""
-        return None
+        raise NotImplementedError()
 
     @property
-    def fan_speed_list(self) -> list:
+    def fan_speed_list(self):
         """Get the list of available fan speed steps of the vacuum cleaner."""
-        return []
+        raise NotImplementedError()
 
     @property
     def state_attributes(self):
diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py
index aecf1eb3cf1..a3a9bb24314 100644
--- a/homeassistant/components/vacuum/demo.py
+++ b/homeassistant/components/vacuum/demo.py
@@ -7,7 +7,7 @@ https://home-assistant.io/components/demo/
 import logging
 
 from homeassistant.components.vacuum import (
-    ATTR_CLEANED_AREA, DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_FAN_SPEED,
+    ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED,
     SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND,
     SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
     VacuumDevice)
@@ -32,6 +32,7 @@ DEMO_VACUUM_COMPLETE = '0_Ground_floor'
 DEMO_VACUUM_MOST = '1_First_floor'
 DEMO_VACUUM_BASIC = '2_Second_floor'
 DEMO_VACUUM_MINIMAL = '3_Third_floor'
+DEMO_VACUUM_NONE = '4_Fourth_floor'
 
 
 def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -41,6 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
         DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES),
         DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
         DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
+        DemoVacuum(DEMO_VACUUM_NONE, 0),
     ])
 
 
@@ -48,7 +50,7 @@ class DemoVacuum(VacuumDevice):
     """Representation of a demo vacuum."""
 
     # pylint: disable=no-self-use
-    def __init__(self, name, supported_features=None):
+    def __init__(self, name, supported_features):
         """Initialize the vacuum."""
         self._name = name
         self._supported_features = supported_features
@@ -66,7 +68,7 @@ class DemoVacuum(VacuumDevice):
     @property
     def icon(self):
         """Return the icon for the vacuum."""
-        return DEFAULT_ICON
+        return 'mdi:roomba'
 
     @property
     def should_poll(self):
@@ -97,9 +99,7 @@ class DemoVacuum(VacuumDevice):
     @property
     def fan_speed_list(self):
         """Return the status of the vacuum."""
-        if self.supported_features & SUPPORT_FAN_SPEED == 0:
-            return
-
+        assert self.supported_features & SUPPORT_FAN_SPEED != 0
         return FAN_SPEEDS
 
     @property
@@ -118,10 +118,7 @@ class DemoVacuum(VacuumDevice):
     @property
     def supported_features(self):
         """Flag supported features."""
-        if self._supported_features is not None:
-            return self._supported_features
-
-        return super().supported_features
+        return self._supported_features
 
     def turn_on(self, **kwargs):
         """Turn the vacuum on."""
diff --git a/homeassistant/components/vacuum/xiaomi.py b/homeassistant/components/vacuum/xiaomi.py
index 49508d0dce5..2e99e94a7d6 100644
--- a/homeassistant/components/vacuum/xiaomi.py
+++ b/homeassistant/components/vacuum/xiaomi.py
@@ -103,9 +103,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
     def async_service_handler(service):
         """Map services to methods on MiroboVacuum."""
         method = SERVICE_TO_METHOD.get(service.service)
-        if not method:
-            return
-
         params = {key: value for key, value in service.data.items()
                   if key != ATTR_ENTITY_ID}
         entity_ids = service.data.get(ATTR_ENTITY_ID)
@@ -191,19 +188,19 @@ class MiroboVacuum(VacuumDevice):
     @property
     def device_state_attributes(self):
         """Return the specific state attributes of this vacuum cleaner."""
+        attrs = {}
         if self.vacuum_state is not None:
-            attrs = {
+            attrs.update({
                 ATTR_DO_NOT_DISTURB:
                     STATE_ON if self.vacuum_state.dnd else STATE_OFF,
                 # Not working --> 'Cleaning mode':
                 #    STATE_ON if self.vacuum_state.in_cleaning else STATE_OFF,
                 ATTR_CLEANING_TIME: str(self.vacuum_state.clean_time),
-                ATTR_CLEANED_AREA: round(self.vacuum_state.clean_area, 2)}
+                ATTR_CLEANED_AREA: round(self.vacuum_state.clean_area, 2)})
             if self.vacuum_state.got_error:
                 attrs[ATTR_ERROR] = self.vacuum_state.error
-            return attrs
 
-        return {}
+        return attrs
 
     @property
     def is_on(self) -> bool:
@@ -242,15 +239,15 @@ class MiroboVacuum(VacuumDevice):
     def async_turn_off(self, **kwargs):
         """Turn the vacuum off and return to home."""
         yield from self.async_stop()
-        return_home = yield from self.async_return_to_base()
-        if return_home:
-            self._is_on = False
+        yield from self.async_return_to_base()
 
     @asyncio.coroutine
     def async_stop(self, **kwargs):
         """Stop the vacuum cleaner."""
-        yield from self._try_command(
+        stopped = yield from self._try_command(
             "Unable to stop: %s", self._vacuum.stop)
+        if stopped:
+            self._is_on = False
 
     @asyncio.coroutine
     def async_set_fan_speed(self, fan_speed, **kwargs):
@@ -338,17 +335,16 @@ class MiroboVacuum(VacuumDevice):
     @asyncio.coroutine
     def async_update(self):
         """Fetch state from the device."""
-        from mirobo import DeviceException
+        from mirobo import VacuumException
         try:
             state = yield from self.hass.async_add_job(self._vacuum.status)
-
             _LOGGER.debug("Got new state from the vacuum: %s", state.data)
             self.vacuum_state = state
             self._is_on = state.is_on
             self._available = True
-        except DeviceException as ex:
-            _LOGGER.warning("Got exception while fetching the state: %s", ex)
+        except OSError as exc:
+            _LOGGER.error("Got OSError while fetching the state: %s", exc)
             # self._available = False
-        except OSError as ex:
-            _LOGGER.error("Got exception while fetching the state: %s", ex)
+        except VacuumException as exc:
+            _LOGGER.warning("Got exception while fetching the state: %s", exc)
             # self._available = False
diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py
index 445aa0a4e88..2b8fb34b92a 100644
--- a/tests/components/vacuum/test_demo.py
+++ b/tests/components/vacuum/test_demo.py
@@ -9,7 +9,7 @@ from homeassistant.components.vacuum import (
     SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED)
 from homeassistant.components.vacuum.demo import (
     DEMO_VACUUM_BASIC, DEMO_VACUUM_COMPLETE, DEMO_VACUUM_MINIMAL,
-    DEMO_VACUUM_MOST, FAN_SPEEDS)
+    DEMO_VACUUM_MOST, DEMO_VACUUM_NONE, FAN_SPEEDS)
 from homeassistant.const import (
     ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON)
 from homeassistant.setup import setup_component
@@ -20,6 +20,7 @@ ENTITY_VACUUM_BASIC = '{}.{}'.format(DOMAIN, DEMO_VACUUM_BASIC).lower()
 ENTITY_VACUUM_COMPLETE = '{}.{}'.format(DOMAIN, DEMO_VACUUM_COMPLETE).lower()
 ENTITY_VACUUM_MINIMAL = '{}.{}'.format(DOMAIN, DEMO_VACUUM_MINIMAL).lower()
 ENTITY_VACUUM_MOST = '{}.{}'.format(DOMAIN, DEMO_VACUUM_MOST).lower()
+ENTITY_VACUUM_NONE = '{}.{}'.format(DOMAIN, DEMO_VACUUM_NONE).lower()
 
 
 class TestVacuumDemo(unittest.TestCase):
@@ -70,18 +71,30 @@ class TestVacuumDemo(unittest.TestCase):
         self.assertEqual(None, state.attributes.get(ATTR_FAN_SPEED_LIST))
         self.assertEqual(STATE_OFF, state.state)
 
+        state = self.hass.states.get(ENTITY_VACUUM_NONE)
+        self.assertEqual(0, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+        self.assertEqual(None, state.attributes.get(ATTR_STATUS))
+        self.assertEqual(None, state.attributes.get(ATTR_BATTERY_LEVEL))
+        self.assertEqual(None, state.attributes.get(ATTR_FAN_SPEED))
+        self.assertEqual(None, state.attributes.get(ATTR_FAN_SPEED_LIST))
+        self.assertEqual(STATE_OFF, state.state)
+
     def test_methods(self):
         """Test if methods call the services as expected."""
         self.hass.states.set(ENTITY_VACUUM_BASIC, STATE_ON)
+        self.hass.block_till_done()
         self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_BASIC))
 
         self.hass.states.set(ENTITY_VACUUM_BASIC, STATE_OFF)
+        self.hass.block_till_done()
         self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_BASIC))
 
         self.hass.states.set(ENTITY_ID_ALL_VACUUMS, STATE_ON)
+        self.hass.block_till_done()
         self.assertTrue(vacuum.is_on(self.hass))
 
         self.hass.states.set(ENTITY_ID_ALL_VACUUMS, STATE_OFF)
+        self.hass.block_till_done()
         self.assertFalse(vacuum.is_on(self.hass))
 
         vacuum.turn_on(self.hass, ENTITY_VACUUM_COMPLETE)
@@ -128,6 +141,54 @@ class TestVacuumDemo(unittest.TestCase):
         state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
         self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED))
 
+    def test_unsupported_methods(self):
+        """Test service calls for unsupported vacuums."""
+        self.hass.states.set(ENTITY_VACUUM_NONE, STATE_ON)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        vacuum.turn_off(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        vacuum.stop(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        self.hass.states.set(ENTITY_VACUUM_NONE, STATE_OFF)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        vacuum.turn_on(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        vacuum.toggle(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        # Non supported methods:
+        vacuum.start_pause(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE))
+
+        vacuum.locate(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_NONE)
+        self.assertIsNone(state.attributes.get(ATTR_STATUS))
+
+        vacuum.return_to_base(self.hass, ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_NONE)
+        self.assertIsNone(state.attributes.get(ATTR_STATUS))
+
+        vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1],
+                             entity_id=ENTITY_VACUUM_NONE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_NONE)
+        self.assertNotEqual(FAN_SPEEDS[-1],
+                            state.attributes.get(ATTR_FAN_SPEED))
+
     def test_services(self):
         """Test vacuum services."""
         # Test send_command
diff --git a/tests/components/vacuum/test_xiaomi.py b/tests/components/vacuum/test_xiaomi.py
index 5897b76a0be..d5c8cda65c3 100644
--- a/tests/components/vacuum/test_xiaomi.py
+++ b/tests/components/vacuum/test_xiaomi.py
@@ -9,7 +9,7 @@ from homeassistant.components.vacuum import (
     ATTR_BATTERY_ICON,
     ATTR_FAN_SPEED, ATTR_FAN_SPEED_LIST, DOMAIN,
     SERVICE_LOCATE, SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND,
-    SERVICE_SET_FAN_SPEED, SERVICE_STOP,
+    SERVICE_SET_FAN_SPEED, SERVICE_START_PAUSE, SERVICE_STOP,
     SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON)
 from homeassistant.components.vacuum.xiaomi import (
     ATTR_CLEANED_AREA, ATTR_CLEANING_TIME, ATTR_DO_NOT_DISTURB, ATTR_ERROR,
@@ -17,18 +17,20 @@ from homeassistant.components.vacuum.xiaomi import (
     SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP,
     SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL)
 from homeassistant.const import (
-    ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON)
+    ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF,
+    STATE_ON)
 from homeassistant.setup import async_setup_component
 
 
 @pytest.fixture
-def mock_mirobo():
+def mock_mirobo_is_off():
     """Mock mock_mirobo."""
     mock_vacuum = mock.MagicMock()
     mock_vacuum.Vacuum().status().data = {'test': 'raw'}
     mock_vacuum.Vacuum().status().is_on = False
     mock_vacuum.Vacuum().status().fanspeed = 38
-    mock_vacuum.Vacuum().status().got_error = False
+    mock_vacuum.Vacuum().status().got_error = True
+    mock_vacuum.Vacuum().status().error = 'Error message'
     mock_vacuum.Vacuum().status().dnd = True
     mock_vacuum.Vacuum().status().battery = 82
     mock_vacuum.Vacuum().status().clean_area = 123.43218
@@ -42,10 +44,59 @@ def mock_mirobo():
         yield mock_vacuum
 
 
+@pytest.fixture
+def mock_mirobo_is_on():
+    """Mock mock_mirobo."""
+    mock_vacuum = mock.MagicMock()
+    mock_vacuum.Vacuum().status().data = {'test': 'raw'}
+    mock_vacuum.Vacuum().status().is_on = True
+    mock_vacuum.Vacuum().status().fanspeed = 99
+    mock_vacuum.Vacuum().status().got_error = False
+    mock_vacuum.Vacuum().status().dnd = False
+    mock_vacuum.Vacuum().status().battery = 32
+    mock_vacuum.Vacuum().status().clean_area = 133.43218
+    mock_vacuum.Vacuum().status().clean_time = timedelta(
+        hours=2, minutes=55, seconds=34)
+    mock_vacuum.Vacuum().status().state = 'Test Xiaomi Cleaning'
+
+    with mock.patch.dict('sys.modules', {
+        'mirobo': mock_vacuum,
+    }):
+        yield mock_vacuum
+
+
+@pytest.fixture
+def mock_mirobo_errors():
+    """Mock mock_mirobo_errors to simulate a bad vacuum status request."""
+    mock_vacuum = mock.MagicMock()
+    mock_vacuum.Vacuum().status.side_effect = OSError()
+    with mock.patch.dict('sys.modules', {
+        'mirobo': mock_vacuum,
+    }):
+        yield mock_vacuum
+
+
 @asyncio.coroutine
-def test_xiaomi_vacuum(hass, caplog, mock_mirobo):
+def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors):
     """Test vacuum supported features."""
-    entity_name = 'test_vacuum_cleaner'
+    entity_name = 'test_vacuum_cleaner_error'
+    yield from async_setup_component(
+        hass, DOMAIN,
+        {DOMAIN: {CONF_PLATFORM: PLATFORM,
+                  CONF_HOST: '127.0.0.1',
+                  CONF_NAME: entity_name,
+                  CONF_TOKEN: '12345678901234567890123456789012'}})
+
+    assert 'Initializing with host 127.0.0.1 (token 12345...)' in caplog.text
+    assert str(mock_mirobo_errors.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert 'ERROR' in caplog.text
+    assert 'Got OSError while fetching the state' in caplog.text
+
+
+@asyncio.coroutine
+def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off):
+    """Test vacuum supported features."""
+    entity_name = 'test_vacuum_cleaner_1'
     entity_id = '{}.{}'.format(DOMAIN, entity_name)
 
     yield from async_setup_component(
@@ -63,7 +114,7 @@ def test_xiaomi_vacuum(hass, caplog, mock_mirobo):
     assert state.state == STATE_OFF
     assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 1023
     assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON
-    assert state.attributes.get(ATTR_ERROR) is None
+    assert state.attributes.get(ATTR_ERROR) == 'Error message'
     assert (state.attributes.get(ATTR_BATTERY_ICON)
             == 'mdi:battery-charging-80')
     assert state.attributes.get(ATTR_CLEANING_TIME) == '2:35:34'
@@ -75,80 +126,137 @@ def test_xiaomi_vacuum(hass, caplog, mock_mirobo):
     # Call services
     yield from hass.services.async_call(
         DOMAIN, SERVICE_TURN_ON, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().start()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().start()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_TURN_OFF, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().home()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().home()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_TOGGLE, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().start()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().start()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_STOP, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().stop()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().stop()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_START_PAUSE, blocking=True)
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().start()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().home()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().home()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_LOCATE, blocking=True)
-    assert str(mock_mirobo.mock_calls[-2]) == 'call.Vacuum().find()'
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().find()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
+    # Set speed service:
     yield from hass.services.async_call(
         DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True)
-    assert (str(mock_mirobo.mock_calls[-2])
+    assert (str(mock_mirobo_is_off.mock_calls[-2])
             == 'call.Vacuum().set_fan_speed(60)')
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "turbo"}, blocking=True)
+    assert (str(mock_mirobo_is_off.mock_calls[-2])
+            == 'call.Vacuum().set_fan_speed(77)')
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
+
+    assert 'ERROR' not in caplog.text
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "invent"}, blocking=True)
+    assert 'ERROR' in caplog.text
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_SEND_COMMAND,
         {"command": "raw"}, blocking=True)
-    assert (str(mock_mirobo.mock_calls[-2])
+    assert (str(mock_mirobo_is_off.mock_calls[-2])
             == "call.Vacuum().raw_command('raw', None)")
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_SEND_COMMAND,
         {"command": "raw", "params": {"k1": 2}}, blocking=True)
-    assert (str(mock_mirobo.mock_calls[-2])
+    assert (str(mock_mirobo_is_off.mock_calls[-2])
             == "call.Vacuum().raw_command('raw', {'k1': 2})")
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
+
+
+@asyncio.coroutine
+def test_xiaomi_vacuum_specific_services(hass, caplog, mock_mirobo_is_on):
+    """Test vacuum supported features."""
+    entity_name = 'test_vacuum_cleaner_2'
+    entity_id = '{}.{}'.format(DOMAIN, entity_name)
+
+    yield from async_setup_component(
+        hass, DOMAIN,
+        {DOMAIN: {CONF_PLATFORM: PLATFORM,
+                  CONF_HOST: '192.168.1.100',
+                  CONF_NAME: entity_name,
+                  CONF_TOKEN: '12345678901234567890123456789012'}})
+
+    assert 'Initializing with host 192.168.1.100 (token 12345' in caplog.text
+
+    # Check state attributes
+    state = hass.states.get(entity_id)
+    assert state.state == STATE_ON
+    assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 1023
+    assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF
+    assert state.attributes.get(ATTR_ERROR) is None
+    assert (state.attributes.get(ATTR_BATTERY_ICON)
+            == 'mdi:battery-30')
+    assert state.attributes.get(ATTR_CLEANING_TIME) == '2:55:34'
+    assert state.attributes.get(ATTR_CLEANED_AREA) == 133.43
+    assert state.attributes.get(ATTR_FAN_SPEED) == 99
+    assert (state.attributes.get(ATTR_FAN_SPEED_LIST)
+            == ['Quiet', 'Balanced', 'Turbo', 'Max'])
+
+    # Check setting pause
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_START_PAUSE, blocking=True)
+    assert str(mock_mirobo_is_on.mock_calls[-2]) == 'call.Vacuum().pause()'
+    assert str(mock_mirobo_is_on.mock_calls[-1]) == 'call.Vacuum().status()'
 
+    # Xiaomi vacuum specific services:
     yield from hass.services.async_call(
-        DOMAIN, SERVICE_START_REMOTE_CONTROL, {}, blocking=True)
-    assert (str(mock_mirobo.mock_calls[-2])
+        DOMAIN, SERVICE_START_REMOTE_CONTROL,
+        {ATTR_ENTITY_ID: entity_id}, blocking=True)
+    assert (str(mock_mirobo_is_on.mock_calls[-2])
             == "call.Vacuum().manual_start()")
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_on.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_MOVE_REMOTE_CONTROL,
         {"duration": 1000, "rotation": -40, "velocity": -0.1}, blocking=True)
-    assert 'call.Vacuum().manual_control(' in str(mock_mirobo.mock_calls[-2])
-    assert 'duration=1000' in str(mock_mirobo.mock_calls[-2])
-    assert 'rotation=-40' in str(mock_mirobo.mock_calls[-2])
-    assert 'velocity=-0.1' in str(mock_mirobo.mock_calls[-2])
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert ('call.Vacuum().manual_control('
+            in str(mock_mirobo_is_on.mock_calls[-2]))
+    assert 'duration=1000' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert 'rotation=-40' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert 'velocity=-0.1' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert str(mock_mirobo_is_on.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True)
-    assert (str(mock_mirobo.mock_calls[-2])
+    assert (str(mock_mirobo_is_on.mock_calls[-2])
             == "call.Vacuum().manual_stop()")
-    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+    assert str(mock_mirobo_is_on.mock_calls[-1]) == 'call.Vacuum().status()'
 
     yield from hass.services.async_call(
         DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP,
         {"duration": 2000, "rotation": 120, "velocity": 0.1}, blocking=True)
     assert ('call.Vacuum().manual_control_once('
-            in str(mock_mirobo.mock_calls[-2]))
-    assert 'duration=2000' in str(mock_mirobo.mock_calls[-2])
-    assert 'rotation=120' in str(mock_mirobo.mock_calls[-2])
-    assert 'velocity=0.1' in str(mock_mirobo.mock_calls[-2])
+            in str(mock_mirobo_is_on.mock_calls[-2]))
+    assert 'duration=2000' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert 'rotation=120' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert 'velocity=0.1' in str(mock_mirobo_is_on.mock_calls[-2])
+    assert str(mock_mirobo_is_on.mock_calls[-1]) == 'call.Vacuum().status()'
-- 
GitLab