Add new service clean_spot to vacuums (#8862)

* Add new service `clean_spot` to vacuums

    - Add as base component service, with associated support flag to make it optional
    - Implement on Demo vacuum
    - Implement on Xiaomi vacuum
    - Update tests for platforms Demo and Xiaomi
    - Change default icon for vacuums to `mdi:roomba`, but keep the one for the Xiaomi
    - (In a polymer PR: add new service to command toolbar in the 'more-info' card)

* Add `clean_spot` service description

* fix default properties for vacuum component
This commit is contained in:
Eugenio Panadero 2017-08-06 19:23:22 +02:00 committed by Paulus Schoutsen
parent d8ca04a4bc
commit c6aaacbb08
6 changed files with 89 additions and 25 deletions

View file

@ -41,6 +41,7 @@ ATTR_FAN_SPEED_LIST = 'fan_speed_list'
ATTR_PARAMS = 'params' ATTR_PARAMS = 'params'
ATTR_STATUS = 'status' ATTR_STATUS = 'status'
SERVICE_CLEAN_SPOT = 'clean_spot'
SERVICE_LOCATE = 'locate' SERVICE_LOCATE = 'locate'
SERVICE_RETURN_TO_BASE = 'return_to_base' SERVICE_RETURN_TO_BASE = 'return_to_base'
SERVICE_SEND_COMMAND = 'send_command' SERVICE_SEND_COMMAND = 'send_command'
@ -67,6 +68,7 @@ SERVICE_TO_METHOD = {
SERVICE_TOGGLE: {'method': 'async_toggle'}, SERVICE_TOGGLE: {'method': 'async_toggle'},
SERVICE_START_PAUSE: {'method': 'async_start_pause'}, SERVICE_START_PAUSE: {'method': 'async_start_pause'},
SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'}, SERVICE_RETURN_TO_BASE: {'method': 'async_return_to_base'},
SERVICE_CLEAN_SPOT: {'method': 'async_clean_spot'},
SERVICE_LOCATE: {'method': 'async_locate'}, SERVICE_LOCATE: {'method': 'async_locate'},
SERVICE_STOP: {'method': 'async_stop'}, SERVICE_STOP: {'method': 'async_stop'},
SERVICE_SET_FAN_SPEED: {'method': 'async_set_fan_speed', SERVICE_SET_FAN_SPEED: {'method': 'async_set_fan_speed',
@ -76,7 +78,7 @@ SERVICE_TO_METHOD = {
} }
DEFAULT_NAME = 'Vacuum cleaner robot' DEFAULT_NAME = 'Vacuum cleaner robot'
DEFAULT_ICON = 'mdi:google-circles-group' DEFAULT_ICON = 'mdi:roomba'
SUPPORT_TURN_ON = 1 SUPPORT_TURN_ON = 1
SUPPORT_TURN_OFF = 2 SUPPORT_TURN_OFF = 2
@ -88,7 +90,8 @@ SUPPORT_BATTERY = 64
SUPPORT_STATUS = 128 SUPPORT_STATUS = 128
SUPPORT_SEND_COMMAND = 256 SUPPORT_SEND_COMMAND = 256
SUPPORT_LOCATE = 512 SUPPORT_LOCATE = 512
SUPPORT_MAP = 1024 SUPPORT_CLEAN_SPOT = 1024
SUPPORT_MAP = 2048
@bind_hass @bind_hass
@ -126,6 +129,13 @@ def locate(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_LOCATE, data) hass.services.call(DOMAIN, SERVICE_LOCATE, data)
@bind_hass
def clean_spot(hass, entity_id=None):
"""Tell all or specified vacuum to perform a spot clean-up."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_CLEAN_SPOT, data)
@bind_hass @bind_hass
def return_to_base(hass, entity_id=None): def return_to_base(hass, entity_id=None):
"""Tell all or specified vacuum to return to base.""" """Tell all or specified vacuum to return to base."""
@ -222,12 +232,12 @@ class VacuumDevice(ToggleEntity):
@property @property
def status(self): def status(self):
"""Return the status of the vacuum cleaner.""" """Return the status of the vacuum cleaner."""
raise NotImplementedError() return None
@property @property
def battery_level(self): def battery_level(self):
"""Return the battery level of the vacuum cleaner.""" """Return the battery level of the vacuum cleaner."""
raise NotImplementedError() return None
@property @property
def battery_icon(self): def battery_icon(self):
@ -241,7 +251,7 @@ class VacuumDevice(ToggleEntity):
@property @property
def fan_speed(self): def fan_speed(self):
"""Return the fan speed of the vacuum cleaner.""" """Return the fan speed of the vacuum cleaner."""
raise NotImplementedError() return None
@property @property
def fan_speed_list(self): def fan_speed_list(self):
@ -310,6 +320,17 @@ class VacuumDevice(ToggleEntity):
""" """
return self.hass.async_add_job(partial(self.stop, **kwargs)) return self.hass.async_add_job(partial(self.stop, **kwargs))
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
raise NotImplementedError()
def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.clean_spot, **kwargs))
def locate(self, **kwargs): def locate(self, **kwargs):
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""
raise NotImplementedError() raise NotImplementedError()

View file

@ -7,10 +7,10 @@ https://home-assistant.io/components/demo/
import logging import logging
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, ATTR_CLEANED_AREA, DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF,
VacuumDevice) SUPPORT_TURN_ON, VacuumDevice)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,7 +25,8 @@ SUPPORT_MOST_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP | \
SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
SUPPORT_STOP | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_RETURN_HOME | \
SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND | \ SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND | \
SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \
SUPPORT_CLEAN_SPOT
FAN_SPEEDS = ['min', 'medium', 'high', 'max'] FAN_SPEEDS = ['min', 'medium', 'high', 'max']
DEMO_VACUUM_COMPLETE = '0_Ground_floor' DEMO_VACUUM_COMPLETE = '0_Ground_floor'
@ -68,7 +69,7 @@ class DemoVacuum(VacuumDevice):
@property @property
def icon(self): def icon(self):
"""Return the icon for the vacuum.""" """Return the icon for the vacuum."""
return 'mdi:roomba' return DEFAULT_ICON
@property @property
def should_poll(self): def should_poll(self):
@ -149,6 +150,17 @@ class DemoVacuum(VacuumDevice):
self._status = 'Stopping the current task' self._status = 'Stopping the current task'
self.schedule_update_ha_state() self.schedule_update_ha_state()
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
if self.supported_features & SUPPORT_CLEAN_SPOT == 0:
return
self._state = True
self._cleaned_area += 1.32
self._battery_level -= 1
self._status = "Cleaning spot"
self.schedule_update_ha_state()
def locate(self, **kwargs): def locate(self, **kwargs):
"""Turn the vacuum off.""" """Turn the vacuum off."""
if self.supported_features & SUPPORT_LOCATE == 0: if self.supported_features & SUPPORT_LOCATE == 0:

View file

@ -46,6 +46,14 @@ return_to_base:
description: Name of the botvac entity. description: Name of the botvac entity.
example: 'vacuum.xiaomi_vacuum_cleaner' example: 'vacuum.xiaomi_vacuum_cleaner'
clean_spot:
description: Tell the vacuum cleaner to do a spot clean-up.
fields:
entity_id:
description: Name of the botvac entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
send_command: send_command:
description: Send a raw command to the vacuum cleaner. description: Send a raw command to the vacuum cleaner.

View file

@ -12,11 +12,10 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA, DEFAULT_ICON, DOMAIN, PLATFORM_SCHEMA, ATTR_CLEANED_AREA, DOMAIN, PLATFORM_SCHEMA, SUPPORT_BATTERY,
SUPPORT_BATTERY, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE,
SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP,
SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VACUUM_SERVICE_SCHEMA, VacuumDevice)
VACUUM_SERVICE_SCHEMA, VacuumDevice)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
@ -27,7 +26,7 @@ REQUIREMENTS = ['python-mirobo==0.1.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Vacuum cleaner' DEFAULT_NAME = 'Xiaomi Vacuum cleaner'
ICON = DEFAULT_ICON ICON = 'mdi:google-circles-group'
PLATFORM = 'xiaomi' PLATFORM = 'xiaomi'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -76,7 +75,7 @@ SERVICE_TO_METHOD = {
SUPPORT_XIAOMI = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ SUPPORT_XIAOMI = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
SUPPORT_STOP | SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \ SUPPORT_STOP | SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \
SUPPORT_SEND_COMMAND | SUPPORT_LOCATE | \ SUPPORT_SEND_COMMAND | SUPPORT_LOCATE | \
SUPPORT_STATUS | SUPPORT_BATTERY SUPPORT_STATUS | SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT
@asyncio.coroutine @asyncio.coroutine
@ -283,6 +282,13 @@ class MiroboVacuum(VacuumDevice):
if return_home: if return_home:
self._is_on = False self._is_on = False
@asyncio.coroutine
def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
yield from self._try_command(
"Unable to start the vacuum for a spot clean-up: %s",
self._vacuum.spot)
@asyncio.coroutine @asyncio.coroutine
def async_locate(self, **kwargs): def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.""" """Locate the vacuum cleaner."""

View file

@ -39,7 +39,7 @@ class TestVacuumDemo(unittest.TestCase):
def test_supported_features(self): def test_supported_features(self):
"""Test vacuum supported features.""" """Test vacuum supported features."""
state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
self.assertEqual(1023, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertEqual(2047, state.attributes.get(ATTR_SUPPORTED_FEATURES))
self.assertEqual("Charging", state.attributes.get(ATTR_STATUS)) self.assertEqual("Charging", state.attributes.get(ATTR_STATUS))
self.assertEqual(100, state.attributes.get(ATTR_BATTERY_LEVEL)) self.assertEqual(100, state.attributes.get(ATTR_BATTERY_LEVEL))
self.assertEqual("medium", state.attributes.get(ATTR_FAN_SPEED)) self.assertEqual("medium", state.attributes.get(ATTR_FAN_SPEED))
@ -141,6 +141,12 @@ class TestVacuumDemo(unittest.TestCase):
state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED))
vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_COMPLETE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
self.assertIn("spot", state.attributes.get(ATTR_STATUS))
self.assertEqual(STATE_ON, state.state)
def test_unsupported_methods(self): def test_unsupported_methods(self):
"""Test service calls for unsupported vacuums.""" """Test service calls for unsupported vacuums."""
self.hass.states.set(ENTITY_VACUUM_NONE, STATE_ON) self.hass.states.set(ENTITY_VACUUM_NONE, STATE_ON)
@ -189,6 +195,12 @@ class TestVacuumDemo(unittest.TestCase):
self.assertNotEqual(FAN_SPEEDS[-1], self.assertNotEqual(FAN_SPEEDS[-1],
state.attributes.get(ATTR_FAN_SPEED)) state.attributes.get(ATTR_FAN_SPEED))
vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_BASIC)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_BASIC)
self.assertNotIn("spot", state.attributes.get(ATTR_STATUS))
self.assertEqual(STATE_OFF, state.state)
def test_services(self): def test_services(self):
"""Test vacuum services.""" """Test vacuum services."""
# Test send_command # Test send_command

View file

@ -8,9 +8,9 @@ import pytest
from homeassistant.components.vacuum import ( from homeassistant.components.vacuum import (
ATTR_BATTERY_ICON, ATTR_BATTERY_ICON,
ATTR_FAN_SPEED, ATTR_FAN_SPEED_LIST, DOMAIN, ATTR_FAN_SPEED, ATTR_FAN_SPEED_LIST, DOMAIN,
SERVICE_LOCATE, SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_RETURN_TO_BASE,
SERVICE_SET_FAN_SPEED, SERVICE_START_PAUSE, SERVICE_STOP, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START_PAUSE,
SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) SERVICE_STOP, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.components.vacuum.xiaomi import ( from homeassistant.components.vacuum.xiaomi import (
ATTR_CLEANED_AREA, ATTR_CLEANING_TIME, ATTR_DO_NOT_DISTURB, ATTR_ERROR, ATTR_CLEANED_AREA, ATTR_CLEANING_TIME, ATTR_DO_NOT_DISTURB, ATTR_ERROR,
CONF_HOST, CONF_NAME, CONF_TOKEN, PLATFORM, CONF_HOST, CONF_NAME, CONF_TOKEN, PLATFORM,
@ -112,7 +112,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off):
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_OFF assert state.state == STATE_OFF
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 1023 assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047
assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON
assert state.attributes.get(ATTR_ERROR) == 'Error message' assert state.attributes.get(ATTR_ERROR) == 'Error message'
assert (state.attributes.get(ATTR_BATTERY_ICON) assert (state.attributes.get(ATTR_BATTERY_ICON)
@ -159,6 +159,11 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off):
assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().find()' assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().find()'
assert str(mock_mirobo_is_off.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_CLEAN_SPOT, {}, blocking=True)
assert str(mock_mirobo_is_off.mock_calls[-2]) == 'call.Vacuum().spot()'
assert str(mock_mirobo_is_off.mock_calls[-1]) == 'call.Vacuum().status()'
# Set speed service: # Set speed service:
yield from hass.services.async_call( yield from hass.services.async_call(
DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True) DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True)
@ -193,7 +198,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off):
@asyncio.coroutine @asyncio.coroutine
def test_xiaomi_vacuum_specific_services(hass, caplog, mock_mirobo_is_on): def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on):
"""Test vacuum supported features.""" """Test vacuum supported features."""
entity_name = 'test_vacuum_cleaner_2' entity_name = 'test_vacuum_cleaner_2'
entity_id = '{}.{}'.format(DOMAIN, entity_name) entity_id = '{}.{}'.format(DOMAIN, entity_name)
@ -210,7 +215,7 @@ def test_xiaomi_vacuum_specific_services(hass, caplog, mock_mirobo_is_on):
# Check state attributes # Check state attributes
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 1023 assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047
assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF
assert state.attributes.get(ATTR_ERROR) is None assert state.attributes.get(ATTR_ERROR) is None
assert (state.attributes.get(ATTR_BATTERY_ICON) assert (state.attributes.get(ATTR_BATTERY_ICON)