diff --git a/.coveragerc b/.coveragerc
index e0b52214de61253b59851a201821b1a26dddc20b..67b4d8be25885cc168bcb898bfecdfd1def40dfc 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -535,7 +535,6 @@ omit =
     homeassistant/components/switch/tplink.py
     homeassistant/components/switch/transmission.py
     homeassistant/components/switch/wake_on_lan.py
-    homeassistant/components/switch/xiaomi_vacuum.py
     homeassistant/components/telegram_bot/*
     homeassistant/components/thingspeak.py
     homeassistant/components/tts/amazon_polly.py
diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py
index bc713c9ec06ab03a4f749b6badd68791b29427db..9989b2799cdab3d218f6fc1947a08373c32309ec 100644
--- a/homeassistant/components/hdmi_cec.py
+++ b/homeassistant/components/hdmi_cec.py
@@ -66,7 +66,6 @@ ATTR_TYPE_ID = 'type_id'
 ATTR_VENDOR_NAME = 'vendor_name'
 ATTR_VENDOR_ID = 'vendor_id'
 ATTR_DEVICE = 'device'
-ATTR_COMMAND = 'command'
 ATTR_TYPE = 'type'
 ATTR_KEY = 'key'
 ATTR_DUR = 'dur'
diff --git a/homeassistant/components/switch/xiaomi_vacuum.py b/homeassistant/components/switch/xiaomi_vacuum.py
deleted file mode 100644
index 393cabb72b9caced7e2f84670bb14108403b97de..0000000000000000000000000000000000000000
--- a/homeassistant/components/switch/xiaomi_vacuum.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""
-Support for Xiaomi Vacuum cleaner robot.
-
-For more details about this platform, please refer to the documentation
-https://home-assistant.io/components/switch.xiaomi_vacuum/
-"""
-import logging
-
-import voluptuous as vol
-
-import homeassistant.helpers.config_validation as cv
-from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
-from homeassistant.const import (DEVICE_DEFAULT_NAME,
-                                 CONF_NAME, CONF_HOST, CONF_TOKEN)
-
-_LOGGER = logging.getLogger(__name__)
-
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
-    vol.Required(CONF_HOST): cv.string,
-    vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
-    vol.Optional(CONF_NAME): cv.string,
-})
-
-REQUIREMENTS = ['python-mirobo==0.1.2']
-
-
-# pylint: disable=unused-argument
-def setup_platform(hass, config, add_devices_callback, discovery_info=None):
-    """Set up the vacuum from config."""
-    host = config.get(CONF_HOST)
-    name = config.get(CONF_NAME)
-    token = config.get(CONF_TOKEN)
-
-    add_devices_callback([MiroboSwitch(name, host, token)], True)
-
-
-class MiroboSwitch(SwitchDevice):
-    """Representation of a Xiaomi Vacuum."""
-
-    def __init__(self, name, host, token):
-        """Initialize the vacuum switch."""
-        self._name = name or DEVICE_DEFAULT_NAME
-        self._icon = 'mdi:broom'
-        self.host = host
-        self.token = token
-
-        self._vacuum = None
-        self._state = None
-        self._state_attrs = {}
-        self._is_on = False
-
-    @property
-    def name(self):
-        """Return the name of the device if any."""
-        return self._name
-
-    @property
-    def icon(self):
-        """Return the icon to use for device if any."""
-        return self._icon
-
-    @property
-    def available(self):
-        """Return true when state is known."""
-        return self._state is not None
-
-    @property
-    def device_state_attributes(self):
-        """Return the state attributes of the device."""
-        return self._state_attrs
-
-    @property
-    def is_on(self):
-        """Return true if switch is on."""
-        return self._is_on
-
-    @property
-    def vacuum(self):
-        """Property accessor for vacuum object."""
-        if not self._vacuum:
-            from mirobo import Vacuum
-            _LOGGER.info("initializing with host %s token %s",
-                         self.host, self.token)
-            self._vacuum = Vacuum(self.host, self.token)
-
-        return self._vacuum
-
-    def turn_on(self, **kwargs):
-        """Turn the vacuum on."""
-        from mirobo import VacuumException
-        try:
-            self.vacuum.start()
-            self._is_on = True
-        except VacuumException as ex:
-            _LOGGER.error("Unable to start the vacuum: %s", ex)
-
-    def turn_off(self, **kwargs):
-        """Turn the vacuum off and return to home."""
-        from mirobo import VacuumException
-        try:
-            self.vacuum.stop()
-            self.vacuum.home()
-            self._is_on = False
-        except VacuumException as ex:
-            _LOGGER.error("Unable to turn off and return home: %s", ex)
-
-    def update(self):
-        """Fetch state from the device."""
-        from mirobo import DeviceException
-        try:
-            state = self.vacuum.status()
-            _LOGGER.debug("got state from the vacuum: %s", state)
-
-            self._state_attrs = {
-                'Status': state.state, 'Error': state.error,
-                'Battery': state.battery, 'Fan': state.fanspeed,
-                'Cleaning time': str(state.clean_time),
-                'Cleaned area': state.clean_area}
-
-            self._state = state.state_code
-            self._is_on = state.is_on
-        except DeviceException as ex:
-            _LOGGER.error("Got exception while fetching the state: %s", ex)
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index b58bdb8916b8f84c913c55ee3ae832ea8afa57ae..38669ff4ee66ce4f5f3de968006003ba6dfee517 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -15,11 +15,11 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth
 import voluptuous as vol
 
 from homeassistant.components.notify import (
-    ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA)
+    ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE)
 from homeassistant.config import load_yaml_config_file
 from homeassistant.const import (
-    CONF_PLATFORM, CONF_API_KEY, CONF_TIMEOUT, ATTR_LATITUDE, ATTR_LONGITUDE,
-    HTTP_DIGEST_AUTHENTICATION)
+    ATTR_COMMAND, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY,
+    CONF_PLATFORM, CONF_TIMEOUT, HTTP_DIGEST_AUTHENTICATION)
 import homeassistant.helpers.config_validation as cv
 from homeassistant.exceptions import TemplateError
 from homeassistant.setup import async_prepare_setup_platform
@@ -35,7 +35,6 @@ ATTR_CALLBACK_QUERY_ID = 'callback_query_id'
 ATTR_CAPTION = 'caption'
 ATTR_CHAT_ID = 'chat_id'
 ATTR_CHAT_INSTANCE = 'chat_instance'
-ATTR_COMMAND = 'command'
 ATTR_DISABLE_NOTIF = 'disable_notification'
 ATTR_DISABLE_WEB_PREV = 'disable_web_page_preview'
 ATTR_EDITED_MSG = 'edited_message'
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..078be8e259f28d2ed7a05a5ee7c3d698cb9e5fe0
--- /dev/null
+++ b/homeassistant/components/vacuum/__init__.py
@@ -0,0 +1,364 @@
+"""
+Support for vacuum cleaner robots (botvacs).
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/vacuum/
+"""
+import asyncio
+from datetime import timedelta
+from functools import partial
+import logging
+import os
+
+import voluptuous as vol
+
+from homeassistant.components import group
+from homeassistant.config import load_yaml_config_file
+from homeassistant.const import (
+    ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE,
+    SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
+from homeassistant.loader import bind_hass
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.config_validation import PLATFORM_SCHEMA  # noqa
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity import ToggleEntity
+from homeassistant.util.icon import icon_for_battery_level
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'vacuum'
+DEPENDENCIES = ['group']
+
+SCAN_INTERVAL = timedelta(seconds=20)
+
+GROUP_NAME_ALL_VACUUMS = 'all vacuum cleaners'
+ENTITY_ID_ALL_VACUUMS = group.ENTITY_ID_FORMAT.format('all_vacuum_cleaners')
+
+ATTR_BATTERY_ICON = 'battery_icon'
+ATTR_CLEANED_AREA = 'cleaned_area'
+ATTR_FAN_SPEED = 'fan_speed'
+ATTR_FAN_SPEED_LIST = 'fan_speed_list'
+ATTR_PARAMS = 'params'
+ATTR_STATUS = 'status'
+
+SERVICE_LOCATE = 'locate'
+SERVICE_RETURN_TO_BASE = 'return_to_base'
+SERVICE_SEND_COMMAND = 'send_command'
+SERVICE_SET_FAN_SPEED = 'set_fan_speed'
+SERVICE_START_PAUSE = 'start_pause'
+SERVICE_STOP = 'stop'
+
+VACUUM_SERVICE_SCHEMA = vol.Schema({
+    vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
+VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({
+    vol.Required(ATTR_FAN_SPEED): cv.string,
+})
+
+VACUUM_SEND_COMMAND_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({
+    vol.Required(ATTR_COMMAND): cv.string,
+    vol.Optional(ATTR_PARAMS): cv.Dict,
+})
+
+SERVICE_TO_METHOD = {
+    SERVICE_TURN_ON: {'method': 'async_turn_on'},
+    SERVICE_TURN_OFF: {'method': 'async_turn_off'},
+    SERVICE_TOGGLE: {'method': 'async_toggle'},
+    SERVICE_START_PAUSE: {'method': 'async_start_pause'},
+    SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'},
+    SERVICE_LOCATE: {'method': 'async_locate'},
+    SERVICE_STOP: {'method': 'async_stop'},
+    SERVICE_SET_FAN_SPEED: {'method': 'async_set_fan_speed',
+                            'schema': VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA},
+    SERVICE_SEND_COMMAND: {'method': 'async_send_command',
+                           'schema': VACUUM_SEND_COMMAND_SERVICE_SCHEMA},
+}
+
+DEFAULT_NAME = 'Vacuum cleaner robot'
+DEFAULT_ICON = 'mdi:google-circles-group'
+
+SUPPORT_TURN_ON = 1
+SUPPORT_TURN_OFF = 2
+SUPPORT_PAUSE = 4
+SUPPORT_STOP = 8
+SUPPORT_RETURN_HOME = 16
+SUPPORT_FAN_SPEED = 32
+SUPPORT_BATTERY = 64
+SUPPORT_STATUS = 128
+SUPPORT_SEND_COMMAND = 256
+SUPPORT_LOCATE = 512
+SUPPORT_MAP = 1024
+
+
+@bind_hass
+def is_on(hass, entity_id=None):
+    """Return if the vacuum is on based on the statemachine."""
+    entity_id = entity_id or ENTITY_ID_ALL_VACUUMS
+    return hass.states.is_state(entity_id, STATE_ON)
+
+
+@bind_hass
+def turn_on(hass, entity_id=None):
+    """Turn all or specified vacuum on."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
+
+
+@bind_hass
+def turn_off(hass, entity_id=None):
+    """Turn all or specified vacuum off."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
+
+
+@bind_hass
+def toggle(hass, entity_id=None):
+    """Toggle all or specified vacuum."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
+
+
+@bind_hass
+def locate(hass, entity_id=None):
+    """Locate all or specified vacuum."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_LOCATE, data)
+
+
+@bind_hass
+def return_to_base(hass, entity_id=None):
+    """Tell all or specified vacuum to return to base."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_RETURN_TO_BASE, data)
+
+
+@bind_hass
+def start_pause(hass, entity_id=None):
+    """Tell all or specified vacuum to start or pause the current task."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_START_PAUSE, data)
+
+
+@bind_hass
+def stop(hass, entity_id=None):
+    """Stop all or specified vacuum."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
+    hass.services.call(DOMAIN, SERVICE_STOP, data)
+
+
+@bind_hass
+def set_fan_speed(hass, fan_speed, entity_id=None):
+    """Set fan speed for all or specified vacuum."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
+    data[ATTR_FAN_SPEED] = fan_speed
+    hass.services.call(DOMAIN, SERVICE_SET_FAN_SPEED, data)
+
+
+@bind_hass
+def send_command(hass, command, params=None, entity_id=None):
+    """Send command to all or specified vacuum."""
+    data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
+    data[ATTR_COMMAND] = command
+    if params is not None:
+        data[ATTR_PARAMS] = params
+    hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
+
+
+@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)
+
+    yield from component.async_setup(config)
+
+    descriptions = yield from hass.async_add_job(
+        load_yaml_config_file, os.path.join(
+            os.path.dirname(__file__), 'services.yaml'))
+
+    @asyncio.coroutine
+    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)
+
+        update_tasks = []
+        for vacuum in target_vacuums:
+            yield from getattr(vacuum, method['method'])(**params)
+            if not vacuum.should_poll:
+                continue
+
+            update_coro = hass.async_add_job(
+                vacuum.async_update_ha_state(True))
+            if hasattr(vacuum, 'async_update'):
+                update_tasks.append(update_coro)
+            else:
+                yield from update_coro
+
+        if update_tasks:
+            yield from asyncio.wait(update_tasks, loop=hass.loop)
+
+    for service in SERVICE_TO_METHOD:
+        schema = SERVICE_TO_METHOD[service].get(
+            'schema', VACUUM_SERVICE_SCHEMA)
+        hass.services.async_register(
+            DOMAIN, service, async_handle_vacuum_service,
+            descriptions.get(service), schema=schema)
+
+    return True
+
+
+class VacuumDevice(ToggleEntity):
+    """Representation of a vacuum cleaner robot."""
+
+    @property
+    def supported_features(self):
+        """Flag vacuum cleaner features that are supported."""
+        return 0
+
+    @property
+    def status(self):
+        """Return the status of the vacuum cleaner."""
+        return None
+
+    @property
+    def battery_level(self):
+        """Return the battery level of the vacuum cleaner."""
+        return None
+
+    @property
+    def battery_icon(self):
+        """Return the battery icon for the vacuum cleaner."""
+        charging = False
+        if self.status is not None:
+            charging = 'charg' in self.status.lower()
+        return icon_for_battery_level(
+            battery_level=self.battery_level, charging=charging)
+
+    @property
+    def fan_speed(self):
+        """Return the fan speed of the vacuum cleaner."""
+        return None
+
+    @property
+    def fan_speed_list(self) -> list:
+        """Get the list of available fan speed steps of the vacuum cleaner."""
+        return []
+
+    @property
+    def state_attributes(self):
+        """Return the state attributes of the vacuum cleaner."""
+        data = {}
+
+        if self.status is not None:
+            data[ATTR_STATUS] = self.status
+
+        if self.battery_level is not None:
+            data[ATTR_BATTERY_LEVEL] = self.battery_level
+            data[ATTR_BATTERY_ICON] = self.battery_icon
+
+        if self.fan_speed is not None:
+            data[ATTR_FAN_SPEED] = self.fan_speed
+            data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list
+
+        return data
+
+    def turn_on(self, **kwargs):
+        """Turn the vacuum on and start cleaning."""
+        raise NotImplementedError()
+
+    def async_turn_on(self, **kwargs):
+        """Turn the vacuum on and start cleaning.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(partial(self.turn_on, **kwargs))
+
+    def turn_off(self, **kwargs):
+        """Turn the vacuum off stopping the cleaning and returning home."""
+        raise NotImplementedError()
+
+    def async_turn_off(self, **kwargs):
+        """Turn the vacuum off stopping the cleaning and returning home.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(partial(self.turn_off, **kwargs))
+
+    def return_to_base(self, **kwargs):
+        """Set the vacuum cleaner to return to the dock."""
+        raise NotImplementedError()
+
+    def async_return_to_base(self, **kwargs):
+        """Set the vacuum cleaner to return to the dock.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(partial(self.return_to_base, **kwargs))
+
+    def stop(self, **kwargs):
+        """Stop the vacuum cleaner."""
+        raise NotImplementedError()
+
+    def async_stop(self, **kwargs):
+        """Stop the vacuum cleaner.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(partial(self.stop, **kwargs))
+
+    def locate(self, **kwargs):
+        """Locate the vacuum cleaner."""
+        raise NotImplementedError()
+
+    def async_locate(self, **kwargs):
+        """Locate the vacuum cleaner.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(partial(self.locate, **kwargs))
+
+    def set_fan_speed(self, fan_speed, **kwargs):
+        """Set fan speed."""
+        raise NotImplementedError()
+
+    def async_set_fan_speed(self, fan_speed, **kwargs):
+        """Set fan speed.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(
+            partial(self.set_fan_speed, fan_speed, **kwargs))
+
+    def start_pause(self, **kwargs):
+        """Start, pause or resume the cleaning task."""
+        raise NotImplementedError()
+
+    def async_start_pause(self, **kwargs):
+        """Start, pause or resume the cleaning task.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(
+            partial(self.start_pause, **kwargs))
+
+    def send_command(self, command, params=None, **kwargs):
+        """Send a command to a vacuum cleaner."""
+        raise NotImplementedError()
+
+    def async_send_command(self, command, params=None, **kwargs):
+        """Send a command to a vacuum cleaner.
+
+        This method must be run in the event loop and returns a coroutine.
+        """
+        return self.hass.async_add_job(
+            partial(self.send_command, command, params=params, **kwargs))
diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..aecf1eb3cf17618058ed0dca27d0b6de57bfbe91
--- /dev/null
+++ b/homeassistant/components/vacuum/demo.py
@@ -0,0 +1,203 @@
+"""
+Demo platform for the vacuum component.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/demo/
+"""
+import logging
+
+from homeassistant.components.vacuum import (
+    ATTR_CLEANED_AREA, DEFAULT_ICON, 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)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_MINIMAL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF
+
+SUPPORT_BASIC_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
+                         SUPPORT_STATUS | SUPPORT_BATTERY
+
+SUPPORT_MOST_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP | \
+                        SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY
+
+SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
+                       SUPPORT_STOP | SUPPORT_RETURN_HOME | \
+                       SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND | \
+                       SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY
+
+FAN_SPEEDS = ['min', 'medium', 'high', 'max']
+DEMO_VACUUM_COMPLETE = '0_Ground_floor'
+DEMO_VACUUM_MOST = '1_First_floor'
+DEMO_VACUUM_BASIC = '2_Second_floor'
+DEMO_VACUUM_MINIMAL = '3_Third_floor'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+    """Set up the Demo vacuums."""
+    add_devices([
+        DemoVacuum(DEMO_VACUUM_COMPLETE, SUPPORT_ALL_SERVICES),
+        DemoVacuum(DEMO_VACUUM_MOST, SUPPORT_MOST_SERVICES),
+        DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
+        DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
+    ])
+
+
+class DemoVacuum(VacuumDevice):
+    """Representation of a demo vacuum."""
+
+    # pylint: disable=no-self-use
+    def __init__(self, name, supported_features=None):
+        """Initialize the vacuum."""
+        self._name = name
+        self._supported_features = supported_features
+        self._state = False
+        self._status = 'Charging'
+        self._fan_speed = FAN_SPEEDS[1]
+        self._cleaned_area = 0
+        self._battery_level = 100
+
+    @property
+    def name(self):
+        """Return the name of the vacuum."""
+        return self._name
+
+    @property
+    def icon(self):
+        """Return the icon for the vacuum."""
+        return DEFAULT_ICON
+
+    @property
+    def should_poll(self):
+        """No polling needed for a demo vacuum."""
+        return False
+
+    @property
+    def is_on(self):
+        """Return true if vacuum is on."""
+        return self._state
+
+    @property
+    def status(self):
+        """Return the status of the vacuum."""
+        if self.supported_features & SUPPORT_STATUS == 0:
+            return
+
+        return self._status
+
+    @property
+    def fan_speed(self):
+        """Return the status of the vacuum."""
+        if self.supported_features & SUPPORT_FAN_SPEED == 0:
+            return
+
+        return self._fan_speed
+
+    @property
+    def fan_speed_list(self):
+        """Return the status of the vacuum."""
+        if self.supported_features & SUPPORT_FAN_SPEED == 0:
+            return
+
+        return FAN_SPEEDS
+
+    @property
+    def battery_level(self):
+        """Return the status of the vacuum."""
+        if self.supported_features & SUPPORT_BATTERY == 0:
+            return
+
+        return max(0, min(100, self._battery_level))
+
+    @property
+    def device_state_attributes(self):
+        """Return device state attributes."""
+        return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
+
+    @property
+    def supported_features(self):
+        """Flag supported features."""
+        if self._supported_features is not None:
+            return self._supported_features
+
+        return super().supported_features
+
+    def turn_on(self, **kwargs):
+        """Turn the vacuum on."""
+        if self.supported_features & SUPPORT_TURN_ON == 0:
+            return
+
+        self._state = True
+        self._cleaned_area += 5.32
+        self._battery_level -= 2
+        self._status = 'Cleaning'
+        self.schedule_update_ha_state()
+
+    def turn_off(self, **kwargs):
+        """Turn the vacuum off."""
+        if self.supported_features & SUPPORT_TURN_OFF == 0:
+            return
+
+        self._state = False
+        self._status = 'Charging'
+        self.schedule_update_ha_state()
+
+    def stop(self, **kwargs):
+        """Turn the vacuum off."""
+        if self.supported_features & SUPPORT_STOP == 0:
+            return
+
+        self._state = False
+        self._status = 'Stopping the current task'
+        self.schedule_update_ha_state()
+
+    def locate(self, **kwargs):
+        """Turn the vacuum off."""
+        if self.supported_features & SUPPORT_LOCATE == 0:
+            return
+
+        self._status = "Hi, I'm over here!"
+        self.schedule_update_ha_state()
+
+    def start_pause(self, **kwargs):
+        """Start, pause or resume the cleaning task."""
+        if self.supported_features & SUPPORT_PAUSE == 0:
+            return
+
+        self._state = not self._state
+        if self._state:
+            self._status = 'Resuming the current task'
+            self._cleaned_area += 1.32
+            self._battery_level -= 1
+        else:
+            self._status = 'Pausing the current task'
+        self.schedule_update_ha_state()
+
+    def set_fan_speed(self, fan_speed, **kwargs):
+        """Tell the vacuum to return to its dock."""
+        if self.supported_features & SUPPORT_FAN_SPEED == 0:
+            return
+
+        if fan_speed in self.fan_speed_list:
+            self._fan_speed = fan_speed
+            self.schedule_update_ha_state()
+
+    def return_to_base(self, **kwargs):
+        """Tell the vacuum to return to its dock."""
+        if self.supported_features & SUPPORT_RETURN_HOME == 0:
+            return
+
+        self._state = False
+        self._status = 'Returning home...'
+        self._battery_level += 5
+        self.schedule_update_ha_state()
+
+    def send_command(self, command, params=None, **kwargs):
+        """Send a command to the vacuum."""
+        if self.supported_features & SUPPORT_SEND_COMMAND == 0:
+            return
+
+        self._status = 'Executing {}({})'.format(command, params)
+        self._state = True
+        self.schedule_update_ha_state()
diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a866794e3ac476ad8fd5e3f46d79c3ae0e32f47c
--- /dev/null
+++ b/homeassistant/components/vacuum/services.yaml
@@ -0,0 +1,131 @@
+turn_on:
+  description: Start a new cleaning task.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+turn_off:
+  description: Stop the current cleaning task and return to home.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+stop:
+  description: Stop the current cleaning task.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+locate:
+  description: Locate the vacuum cleaner robot.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+start_pause:
+  description: Start, pause, or resume the cleaning task.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+return_to_base:
+  description: Tell the vacuum cleaner to return to its dock.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+send_command:
+  description: Send a raw command to the vacuum cleaner.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+    command:
+      description: Command to execute.
+      example: 'set_dnd_timer'
+
+    params:
+      description: Parameters for the command.
+      example: '[22,0,6,0]'
+
+set_fan_speed:
+  description: Set the fan speed of the vacuum cleaner.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+    fan_speed:
+      description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium', or by percentage, between 0 and 100.
+      example: 'low'
+
+xiaomi_remote_control_start:
+  description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+xiaomi_remote_control_stop:
+  description: Stop remote control mode of the vacuum cleaner.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+xiaomi_remote_control_move:
+  description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+    velocity:
+      description: Speed, between -0.29 and 0.29.
+      example: '0.2'
+
+    rotation:
+      description: Rotation, between -179 degrees and 179 degrees.
+      example: '90'
+
+    duration:
+      description: Duration of the movement?
+      example: '1500'
+
+xiaomi_remote_control_move_step:
+  description: Remote control the vacuum cleaner, only makes one move and then stops.
+
+  fields:
+    entity_id:
+      description: Name of the botvac entity.
+      example: 'vacuum.xiaomi_vacuum_cleaner'
+
+    velocity:
+      description: Speed, between -0.29 and 0.29.
+      example: '0.2'
+
+    rotation:
+      description: Rotation, between -179 degrees and 179 degrees.
+      example: '90'
+
+    duration:
+      description: Duration of the movement?
+      example: '1500'
diff --git a/homeassistant/components/vacuum/xiaomi.py b/homeassistant/components/vacuum/xiaomi.py
new file mode 100644
index 0000000000000000000000000000000000000000..49508d0dce5a349f1967518cb9a9429afde150ab
--- /dev/null
+++ b/homeassistant/components/vacuum/xiaomi.py
@@ -0,0 +1,354 @@
+"""
+Support for the Xiaomi vacuum cleaner robot.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/vacuum.xiaomi/
+"""
+import asyncio
+from functools import partial
+import logging
+import os
+
+import voluptuous as vol
+
+from homeassistant.components.vacuum import (
+    ATTR_CLEANED_AREA, DEFAULT_ICON, DOMAIN, PLATFORM_SCHEMA,
+    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,
+    VACUUM_SERVICE_SCHEMA, VacuumDevice)
+from homeassistant.config import load_yaml_config_file
+from homeassistant.const import (
+    ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['python-mirobo==0.1.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'Xiaomi Vacuum cleaner'
+ICON = DEFAULT_ICON
+PLATFORM = 'xiaomi'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Required(CONF_HOST): cv.string,
+    vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
+    vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+}, extra=vol.ALLOW_EXTRA)
+
+SERVICE_MOVE_REMOTE_CONTROL = 'xiaomi_remote_control_move'
+SERVICE_MOVE_REMOTE_CONTROL_STEP = 'xiaomi_remote_control_move_step'
+SERVICE_START_REMOTE_CONTROL = 'xiaomi_remote_control_start'
+SERVICE_STOP_REMOTE_CONTROL = 'xiaomi_remote_control_stop'
+
+FAN_SPEEDS = {
+    'Quiet': 38,
+    'Balanced': 60,
+    'Turbo': 77,
+    'Max': 90}
+
+ATTR_CLEANING_TIME = 'cleaning_time'
+ATTR_DO_NOT_DISTURB = 'do_not_disturb'
+ATTR_ERROR = 'error'
+ATTR_RC_DURATION = 'duration'
+ATTR_RC_ROTATION = 'rotation'
+ATTR_RC_VELOCITY = 'velocity'
+
+SERVICE_SCHEMA_REMOTE_CONTROL = VACUUM_SERVICE_SCHEMA.extend({
+    vol.Optional(ATTR_RC_VELOCITY):
+        vol.All(vol.Coerce(float), vol.Clamp(min=-0.29, max=0.29)),
+    vol.Optional(ATTR_RC_ROTATION):
+        vol.All(vol.Coerce(int), vol.Clamp(min=-179, max=179)),
+    vol.Optional(ATTR_RC_DURATION): cv.positive_int,
+})
+
+SERVICE_TO_METHOD = {
+    SERVICE_START_REMOTE_CONTROL: {'method': 'async_remote_control_start'},
+    SERVICE_STOP_REMOTE_CONTROL: {'method': 'async_remote_control_stop'},
+    SERVICE_MOVE_REMOTE_CONTROL: {
+        'method': 'async_remote_control_move',
+        'schema': SERVICE_SCHEMA_REMOTE_CONTROL},
+    SERVICE_MOVE_REMOTE_CONTROL_STEP: {
+        'method': 'async_remote_control_move_step',
+        'schema': SERVICE_SCHEMA_REMOTE_CONTROL},
+}
+
+SUPPORT_XIAOMI = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
+                 SUPPORT_STOP | SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \
+                 SUPPORT_SEND_COMMAND | SUPPORT_LOCATE | \
+                 SUPPORT_STATUS | SUPPORT_BATTERY
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+    """Set up the Xiaomi vacuum cleaner robot platform."""
+    from mirobo import Vacuum
+    if PLATFORM not in hass.data:
+        hass.data[PLATFORM] = {}
+
+    host = config.get(CONF_HOST)
+    name = config.get(CONF_NAME)
+    token = config.get(CONF_TOKEN)
+
+    # Create handler
+    _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
+    vacuum = Vacuum(host, token)
+
+    mirobo = MiroboVacuum(name, vacuum)
+    hass.data[PLATFORM][host] = mirobo
+
+    async_add_devices([mirobo], update_before_add=True)
+
+    @asyncio.coroutine
+    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)
+        if entity_ids:
+            target_vacuums = [vac for vac in hass.data[PLATFORM].values()
+                              if vac.entity_id in entity_ids]
+        else:
+            target_vacuums = hass.data[PLATFORM].values()
+
+        update_tasks = []
+        for vacuum in target_vacuums:
+            yield from getattr(vacuum, method['method'])(**params)
+
+        for vacuum in target_vacuums:
+            update_coro = vacuum.async_update_ha_state(True)
+            update_tasks.append(update_coro)
+
+        if update_tasks:
+            yield from asyncio.wait(update_tasks, loop=hass.loop)
+
+    descriptions = yield from hass.async_add_job(
+        load_yaml_config_file, os.path.join(
+            os.path.dirname(__file__), 'services.yaml'))
+
+    for vacuum_service in SERVICE_TO_METHOD:
+        schema = SERVICE_TO_METHOD[vacuum_service].get(
+            'schema', VACUUM_SERVICE_SCHEMA)
+        hass.services.async_register(
+            DOMAIN, vacuum_service, async_service_handler,
+            description=descriptions.get(vacuum_service), schema=schema)
+
+
+class MiroboVacuum(VacuumDevice):
+    """Representation of a Xiaomi Vacuum cleaner robot."""
+
+    def __init__(self, name, vacuum):
+        """Initialize the Xiaomi vacuum cleaner robot handler."""
+        self._name = name
+        self._icon = ICON
+        self._vacuum = vacuum
+
+        self.vacuum_state = None
+        self._is_on = False
+        self._available = False
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return self._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."""
+        if self.vacuum_state is not None:
+            return self.vacuum_state.state
+
+    @property
+    def battery_level(self):
+        """Return the battery level of the vacuum cleaner."""
+        if self.vacuum_state is not None:
+            return self.vacuum_state.battery
+
+    @property
+    def fan_speed(self):
+        """Return the fan speed of the vacuum cleaner."""
+        if self.vacuum_state is not None:
+            speed = self.vacuum_state.fanspeed
+            if speed in FAN_SPEEDS.values():
+                return [key for key, value in FAN_SPEEDS.items()
+                        if value == speed][0]
+            return speed
+
+    @property
+    def fan_speed_list(self):
+        """Get the list of available fan speed steps of the vacuum cleaner."""
+        return list(sorted(FAN_SPEEDS.keys(), key=lambda s: FAN_SPEEDS[s]))
+
+    @property
+    def device_state_attributes(self):
+        """Return the specific state attributes of this vacuum cleaner."""
+        if self.vacuum_state is not None:
+            attrs = {
+                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)}
+            if self.vacuum_state.got_error:
+                attrs[ATTR_ERROR] = self.vacuum_state.error
+            return attrs
+
+        return {}
+
+    @property
+    def is_on(self) -> bool:
+        """Return True if entity is on."""
+        return self._is_on
+
+    @property
+    def available(self) -> bool:
+        """Return True if entity is available."""
+        return self._available
+
+    @property
+    def supported_features(self):
+        """Flag vacuum cleaner robot features that are supported."""
+        return SUPPORT_XIAOMI
+
+    @asyncio.coroutine
+    def _try_command(self, mask_error, func, *args, **kwargs):
+        """Call a vacuum command handling error messages."""
+        from mirobo import VacuumException
+        try:
+            yield from self.hass.async_add_job(partial(func, *args, **kwargs))
+            return True
+        except VacuumException as ex:
+            _LOGGER.error(mask_error, ex)
+            return False
+
+    @asyncio.coroutine
+    def async_turn_on(self, **kwargs):
+        """Turn the vacuum on."""
+        is_on = yield from self._try_command(
+            "Unable to start the vacuum: %s", self._vacuum.start)
+        self._is_on = is_on
+
+    @asyncio.coroutine
+    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
+
+    @asyncio.coroutine
+    def async_stop(self, **kwargs):
+        """Stop the vacuum cleaner."""
+        yield from self._try_command(
+            "Unable to stop: %s", self._vacuum.stop)
+
+    @asyncio.coroutine
+    def async_set_fan_speed(self, fan_speed, **kwargs):
+        """Set fan speed."""
+        if fan_speed.capitalize() in FAN_SPEEDS:
+            fan_speed = FAN_SPEEDS[fan_speed.capitalize()]
+        else:
+            try:
+                fan_speed = int(fan_speed)
+            except ValueError as exc:
+                _LOGGER.error("Fan speed step not recognized (%s). "
+                              "Valid speeds are: %s", exc,
+                              self.fan_speed_list)
+                return
+        yield from self._try_command(
+            "Unable to set fan speed: %s",
+            self._vacuum.set_fan_speed, fan_speed)
+
+    @asyncio.coroutine
+    def async_start_pause(self, **kwargs):
+        """Start, pause or resume the cleaning task."""
+        if self.vacuum_state and self.is_on:
+            yield from self._try_command(
+                "Unable to set start/pause: %s", self._vacuum.pause)
+        else:
+            yield from self.async_turn_on()
+
+    @asyncio.coroutine
+    def async_return_to_base(self, **kwargs):
+        """Set the vacuum cleaner to return to the dock."""
+        return_home = yield from self._try_command(
+            "Unable to return home: %s", self._vacuum.home)
+        if return_home:
+            self._is_on = False
+
+    @asyncio.coroutine
+    def async_locate(self, **kwargs):
+        """Locate the vacuum cleaner."""
+        yield from self._try_command(
+            "Unable to locate the botvac: %s", self._vacuum.find)
+
+    @asyncio.coroutine
+    def async_send_command(self, command, params=None, **kwargs):
+        """Send raw command."""
+        yield from self._try_command(
+            "Unable to send command to the vacuum: %s",
+            self._vacuum.raw_command, command, params)
+
+    @asyncio.coroutine
+    def async_remote_control_start(self):
+        """Start remote control mode."""
+        yield from self._try_command(
+            "Unable to start remote control the vacuum: %s",
+            self._vacuum.manual_start)
+
+    @asyncio.coroutine
+    def async_remote_control_stop(self):
+        """Stop remote control mode."""
+        yield from self._try_command(
+            "Unable to stop remote control the vacuum: %s",
+            self._vacuum.manual_stop)
+
+    @asyncio.coroutine
+    def async_remote_control_move(self,
+                                  rotation: int=0,
+                                  velocity: float=0.3,
+                                  duration: int=1500):
+        """Move vacuum with remote control mode."""
+        yield from self._try_command(
+            "Unable to move with remote control the vacuum: %s",
+            self._vacuum.manual_control,
+            velocity=velocity, rotation=rotation, duration=duration)
+
+    @asyncio.coroutine
+    def async_remote_control_move_step(self,
+                                       rotation: int=0,
+                                       velocity: float=0.2,
+                                       duration: int=1500):
+        """Move vacuum one step with remote control mode."""
+        yield from self._try_command(
+            "Unable to remote control the vacuum: %s",
+            self._vacuum.manual_control_once,
+            velocity=velocity, rotation=rotation, duration=duration)
+
+    @asyncio.coroutine
+    def async_update(self):
+        """Fetch state from the device."""
+        from mirobo import DeviceException
+        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)
+            # self._available = False
+        except OSError as ex:
+            _LOGGER.error("Got exception while fetching the state: %s", ex)
+            # self._available = False
diff --git a/homeassistant/const.py b/homeassistant/const.py
index fadd85e6b72856a9dd7fb00da4420de26ae0b2b6..1fdf5b7d51ae831df6c5dd70e2a1de67c6787853 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -283,6 +283,9 @@ ATTR_WAKEUP = 'wake_up_interval'
 ATTR_CODE = 'code'
 ATTR_CODE_FORMAT = 'code_format'
 
+# For calling a device specific command
+ATTR_COMMAND = 'command'
+
 # For devices which support an armed state
 ATTR_ARMED = 'device_armed'
 
diff --git a/homeassistant/util/icon.py b/homeassistant/util/icon.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc8cce647127d98c061c4662739dbe7e9a0afd24
--- /dev/null
+++ b/homeassistant/util/icon.py
@@ -0,0 +1,18 @@
+"""Icon util methods."""
+from typing import Optional
+
+
+def icon_for_battery_level(battery_level: Optional[int]=None,
+                           charging: bool=False) -> str:
+    """Return a battery icon valid identifier."""
+    icon = 'mdi:battery'
+    if battery_level is None:
+        return icon + '-unknown'
+    if charging and battery_level > 10:
+        icon += '-charging-{}'.format(
+            int(round(battery_level / 20 - .01)) * 20)
+    elif charging or battery_level <= 5:
+        icon += '-outline'
+    elif 5 < battery_level < 95:
+        icon += '-{}'.format(int(round(battery_level / 10 - .01)) * 10)
+    return icon
diff --git a/requirements_all.txt b/requirements_all.txt
index 4ea43745cf947425347be196c8761df328a91e69..d253a77e86d491f709bc4cc59e23fbc7e026d2c6 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -728,7 +728,7 @@ python-juicenet==0.0.5
 # homeassistant.components.lirc
 # python-lirc==1.2.3
 
-# homeassistant.components.switch.xiaomi_vacuum
+# homeassistant.components.vacuum.xiaomi
 python-mirobo==0.1.2
 
 # homeassistant.components.media_player.mpd
diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..b62949e6e8a669ab23a4afc621d2959d07e626fe
--- /dev/null
+++ b/tests/components/vacuum/__init__.py
@@ -0,0 +1 @@
+"""The tests for vacuum platforms."""
diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py
new file mode 100644
index 0000000000000000000000000000000000000000..445aa0a4e88356a61d050c6d343d2cbfc98ed417
--- /dev/null
+++ b/tests/components/vacuum/test_demo.py
@@ -0,0 +1,210 @@
+"""The tests for the Demo vacuum platform."""
+import unittest
+
+from homeassistant.components import vacuum
+from homeassistant.components.vacuum import (
+    ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, ATTR_FAN_SPEED,
+    ATTR_FAN_SPEED_LIST, ATTR_PARAMS, ATTR_STATUS, DOMAIN,
+    ENTITY_ID_ALL_VACUUMS,
+    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)
+from homeassistant.const import (
+    ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON)
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant, mock_service
+
+
+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()
+
+
+class TestVacuumDemo(unittest.TestCase):
+    """Test the Demo vacuum."""
+
+    def setUp(self):  # pylint: disable=invalid-name
+        """Setup things to be run when tests are started."""
+        self.hass = get_test_home_assistant()
+        self.assertTrue(setup_component(
+            self.hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: 'demo'}}))
+
+    def tearDown(self):  # pylint: disable=invalid-name
+        """Stop down everything that was started."""
+        self.hass.stop()
+
+    def test_supported_features(self):
+        """Test vacuum supported features."""
+        state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+        self.assertEqual(1023, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+        self.assertEqual("Charging", state.attributes.get(ATTR_STATUS))
+        self.assertEqual(100, state.attributes.get(ATTR_BATTERY_LEVEL))
+        self.assertEqual("medium", state.attributes.get(ATTR_FAN_SPEED))
+        self.assertListEqual(FAN_SPEEDS,
+                             state.attributes.get(ATTR_FAN_SPEED_LIST))
+        self.assertEqual(STATE_OFF, state.state)
+
+        state = self.hass.states.get(ENTITY_VACUUM_MOST)
+        self.assertEqual(219, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+        self.assertEqual("Charging", state.attributes.get(ATTR_STATUS))
+        self.assertEqual(100, 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)
+
+        state = self.hass.states.get(ENTITY_VACUUM_BASIC)
+        self.assertEqual(195, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+        self.assertEqual("Charging", state.attributes.get(ATTR_STATUS))
+        self.assertEqual(100, 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)
+
+        state = self.hass.states.get(ENTITY_VACUUM_MINIMAL)
+        self.assertEqual(3, 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.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_BASIC))
+
+        self.hass.states.set(ENTITY_VACUUM_BASIC, STATE_OFF)
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_BASIC))
+
+        self.hass.states.set(ENTITY_ID_ALL_VACUUMS, STATE_ON)
+        self.assertTrue(vacuum.is_on(self.hass))
+
+        self.hass.states.set(ENTITY_ID_ALL_VACUUMS, STATE_OFF)
+        self.assertFalse(vacuum.is_on(self.hass))
+
+        vacuum.turn_on(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        vacuum.turn_off(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        vacuum.toggle(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        vacuum.stop(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE))
+
+        state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+        self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100)
+        self.assertNotEqual("Charging", state.attributes.get(ATTR_STATUS))
+
+        vacuum.locate(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+        self.assertIn("I'm over here", state.attributes.get(ATTR_STATUS))
+
+        vacuum.return_to_base(self.hass, ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+        self.assertIn("Returning home", state.attributes.get(ATTR_STATUS))
+
+        vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1],
+                             entity_id=ENTITY_VACUUM_COMPLETE)
+        self.hass.block_till_done()
+        state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+        self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED))
+
+    def test_services(self):
+        """Test vacuum services."""
+        # Test send_command
+        send_command_calls = mock_service(
+            self.hass, DOMAIN, SERVICE_SEND_COMMAND)
+
+        params = {"rotate": 150, "speed": 20}
+        vacuum.send_command(
+            self.hass, 'test_command', entity_id=ENTITY_VACUUM_BASIC,
+            params=params)
+
+        self.hass.block_till_done()
+        self.assertEqual(1, len(send_command_calls))
+        call = send_command_calls[-1]
+
+        self.assertEqual(DOMAIN, call.domain)
+        self.assertEqual(SERVICE_SEND_COMMAND, call.service)
+        self.assertEqual(ENTITY_VACUUM_BASIC, call.data[ATTR_ENTITY_ID])
+        self.assertEqual('test_command', call.data[ATTR_COMMAND])
+        self.assertEqual(params, call.data[ATTR_PARAMS])
+
+        # Test set fan speed
+        set_fan_speed_calls = mock_service(
+            self.hass, DOMAIN, SERVICE_SET_FAN_SPEED)
+
+        vacuum.set_fan_speed(
+            self.hass, FAN_SPEEDS[0], entity_id=ENTITY_VACUUM_COMPLETE)
+
+        self.hass.block_till_done()
+        self.assertEqual(1, len(set_fan_speed_calls))
+        call = set_fan_speed_calls[-1]
+
+        self.assertEqual(DOMAIN, call.domain)
+        self.assertEqual(SERVICE_SET_FAN_SPEED, call.service)
+        self.assertEqual(ENTITY_VACUUM_COMPLETE, call.data[ATTR_ENTITY_ID])
+        self.assertEqual(FAN_SPEEDS[0], call.data[ATTR_FAN_SPEED])
+
+    def test_set_fan_speed(self):
+        """Test vacuum service to set the fan speed."""
+        group_vacuums = ','.join([ENTITY_VACUUM_BASIC,
+                                  ENTITY_VACUUM_COMPLETE])
+        old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC)
+        old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+
+        vacuum.set_fan_speed(
+            self.hass, FAN_SPEEDS[0], entity_id=group_vacuums)
+
+        self.hass.block_till_done()
+        new_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC)
+        new_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+
+        self.assertEqual(old_state_basic, new_state_basic)
+        self.assertNotIn(ATTR_FAN_SPEED, new_state_basic.attributes)
+
+        self.assertNotEqual(old_state_complete, new_state_complete)
+        self.assertEqual(FAN_SPEEDS[1],
+                         old_state_complete.attributes[ATTR_FAN_SPEED])
+        self.assertEqual(FAN_SPEEDS[0],
+                         new_state_complete.attributes[ATTR_FAN_SPEED])
+
+    def test_send_command(self):
+        """Test vacuum service to send a command."""
+        group_vacuums = ','.join([ENTITY_VACUUM_BASIC,
+                                  ENTITY_VACUUM_COMPLETE])
+        old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC)
+        old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+
+        vacuum.send_command(
+            self.hass, 'test_command', params={"p1": 3},
+            entity_id=group_vacuums)
+
+        self.hass.block_till_done()
+        new_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC)
+        new_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
+
+        self.assertEqual(old_state_basic, new_state_basic)
+        self.assertNotEqual(old_state_complete, new_state_complete)
+        self.assertEqual(STATE_ON, new_state_complete.state)
+        self.assertEqual("Executing test_command({'p1': 3})",
+                         new_state_complete.attributes[ATTR_STATUS])
diff --git a/tests/components/vacuum/test_xiaomi.py b/tests/components/vacuum/test_xiaomi.py
new file mode 100644
index 0000000000000000000000000000000000000000..5897b76a0be09e3a20d6ef5132b7d6cef1429526
--- /dev/null
+++ b/tests/components/vacuum/test_xiaomi.py
@@ -0,0 +1,154 @@
+"""The tests for the Xiaomi vacuum platform."""
+import asyncio
+from datetime import timedelta
+from unittest import mock
+
+import pytest
+
+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_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,
+    CONF_HOST, CONF_NAME, CONF_TOKEN, PLATFORM,
+    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)
+from homeassistant.setup import async_setup_component
+
+
+@pytest.fixture
+def mock_mirobo():
+    """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().dnd = True
+    mock_vacuum.Vacuum().status().battery = 82
+    mock_vacuum.Vacuum().status().clean_area = 123.43218
+    mock_vacuum.Vacuum().status().clean_time = timedelta(
+        hours=2, minutes=35, seconds=34)
+    mock_vacuum.Vacuum().status().state = 'Test Xiaomi Charging'
+
+    with mock.patch.dict('sys.modules', {
+        'mirobo': mock_vacuum,
+    }):
+        yield mock_vacuum
+
+
+@asyncio.coroutine
+def test_xiaomi_vacuum(hass, caplog, mock_mirobo):
+    """Test vacuum supported features."""
+    entity_name = 'test_vacuum_cleaner'
+    entity_id = '{}.{}'.format(DOMAIN, entity_name)
+
+    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
+
+    # Check state attributes
+    state = hass.states.get(entity_id)
+
+    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_BATTERY_ICON)
+            == 'mdi:battery-charging-80')
+    assert state.attributes.get(ATTR_CLEANING_TIME) == '2:35:34'
+    assert state.attributes.get(ATTR_CLEANED_AREA) == 123.43
+    assert state.attributes.get(ATTR_FAN_SPEED) == 'Quiet'
+    assert (state.attributes.get(ATTR_FAN_SPEED_LIST)
+            == ['Quiet', 'Balanced', 'Turbo', 'Max'])
+
+    # 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()'
+
+    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()'
+
+    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()'
+
+    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()'
+
+    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()'
+
+    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()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True)
+    assert (str(mock_mirobo.mock_calls[-2])
+            == 'call.Vacuum().set_fan_speed(60)')
+    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_SEND_COMMAND,
+        {"command": "raw"}, blocking=True)
+    assert (str(mock_mirobo.mock_calls[-2])
+            == "call.Vacuum().raw_command('raw', None)")
+    assert str(mock_mirobo.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])
+            == "call.Vacuum().raw_command('raw', {'k1': 2})")
+    assert str(mock_mirobo.mock_calls[-1]) == 'call.Vacuum().status()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_START_REMOTE_CONTROL, {}, blocking=True)
+    assert (str(mock_mirobo.mock_calls[-2])
+            == "call.Vacuum().manual_start()")
+    assert str(mock_mirobo.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()'
+
+    yield from hass.services.async_call(
+        DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True)
+    assert (str(mock_mirobo.mock_calls[-2])
+            == "call.Vacuum().manual_stop()")
+    assert str(mock_mirobo.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])
diff --git a/tests/util/test_icon.py b/tests/util/test_icon.py
new file mode 100644
index 0000000000000000000000000000000000000000..2275fdcc6d36266f91f5011dccc709e9c2c84b92
--- /dev/null
+++ b/tests/util/test_icon.py
@@ -0,0 +1,53 @@
+"""Test Home Assistant icon util methods."""
+import unittest
+
+
+class TestIconUtil(unittest.TestCase):
+    """Test icon util methods."""
+
+    def test_battery_icon(self):
+        """Test icon generator for battery sensor."""
+        from homeassistant.util.icon import icon_for_battery_level
+
+        self.assertEqual('mdi:battery-unknown',
+                         icon_for_battery_level(None, True))
+        self.assertEqual('mdi:battery-unknown',
+                         icon_for_battery_level(None, False))
+
+        self.assertEqual('mdi:battery-outline',
+                         icon_for_battery_level(5, True))
+        self.assertEqual('mdi:battery-outline',
+                         icon_for_battery_level(5, False))
+
+        self.assertEqual('mdi:battery-charging-100',
+                         icon_for_battery_level(100, True))
+        self.assertEqual('mdi:battery',
+                         icon_for_battery_level(100, False))
+
+        iconbase = 'mdi:battery'
+        for level in range(0, 100, 5):
+            print('Level: %d. icon: %s, charging: %s'
+                  % (level, icon_for_battery_level(level, False),
+                     icon_for_battery_level(level, True)))
+            if level <= 10:
+                postfix_charging = '-outline'
+            elif level <= 30:
+                postfix_charging = '-charging-20'
+            elif level <= 50:
+                postfix_charging = '-charging-40'
+            elif level <= 70:
+                postfix_charging = '-charging-60'
+            elif level <= 90:
+                postfix_charging = '-charging-80'
+            else:
+                postfix_charging = '-charging-100'
+            if 5 < level < 95:
+                postfix = '-{}'.format(int(round(level / 10 - .01)) * 10)
+            elif level <= 5:
+                postfix = '-outline'
+            else:
+                postfix = ''
+            self.assertEqual(iconbase + postfix,
+                             icon_for_battery_level(level, False))
+            self.assertEqual(iconbase + postfix_charging,
+                             icon_for_battery_level(level, True))