1
mirror of https://github.com/home-assistant/core synced 2024-09-18 19:55:20 +02:00

Add support for STATES of vacuums (#15573)

* Vacuum: Added support for STATES

* Added debug logging and corrected state order

* typo

* Fix travis error, STATE = STATE for readability

* status -> state

* Changed to Entity instead of ToogleEntity

* Updated some vacuums

* Revert changes

* Revert Changes

* added SUPPORT_STATE

* Woof?

* Implement on/off if STATE not supported

* Moved new state vaccum to Class StateVacuumDevice

* Error: I should go to bed

* Moved around methods for easier reading

* Added StateVacuumDevice demo vacuum

* Added tests for StateVacuumDevice demo vacuum

* Fix styling errors

* Refactored to BaseVaccum

* Vacuum will now go back to dock

* Class BaseVacuum is for internal use only

* return -> await

* return -> await
This commit is contained in:
Conrad Juhl Andersen 2018-08-01 14:51:38 +02:00 committed by Jason Hu
parent f8a478946e
commit 2ff5b4ce95
3 changed files with 344 additions and 99 deletions

View File

@ -14,12 +14,12 @@ import voluptuous as vol
from homeassistant.components import group
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON)
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_PAUSED, STATE_IDLE)
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.helpers.entity import (ToggleEntity, Entity)
from homeassistant.helpers.icon import icon_for_battery_level
_LOGGER = logging.getLogger(__name__)
@ -75,6 +75,13 @@ SERVICE_TO_METHOD = {
'schema': VACUUM_SEND_COMMAND_SERVICE_SCHEMA},
}
STATE_CLEANING = 'cleaning'
STATE_DOCKED = 'docked'
STATE_IDLE = STATE_IDLE
STATE_PAUSED = STATE_PAUSED
STATE_RETURNING = 'returning'
STATE_ERROR = 'error'
DEFAULT_NAME = 'Vacuum cleaner robot'
SUPPORT_TURN_ON = 1
@ -89,6 +96,7 @@ SUPPORT_SEND_COMMAND = 256
SUPPORT_LOCATE = 512
SUPPORT_CLEAN_SPOT = 1024
SUPPORT_MAP = 2048
SUPPORT_STATE = 4096
@bind_hass
@ -208,33 +216,22 @@ def async_setup(hass, config):
return True
class VacuumDevice(ToggleEntity):
"""Representation of a vacuum cleaner robot."""
class _BaseVacuum(Entity):
"""Representation of a base vacuum.
Contains common properties and functions for all vacuum devices.
"""
@property
def supported_features(self):
"""Flag vacuum cleaner features that are supported."""
raise NotImplementedError()
@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."""
@ -245,6 +242,106 @@ class VacuumDevice(ToggleEntity):
"""Get the list of available fan speed steps of the vacuum cleaner."""
raise NotImplementedError()
def start_pause(self, **kwargs):
"""Start, pause or resume the cleaning task."""
raise NotImplementedError()
async def async_start_pause(self, **kwargs):
"""Start, pause or resume the cleaning task.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.start_pause, **kwargs))
def stop(self, **kwargs):
"""Stop the vacuum cleaner."""
raise NotImplementedError()
async def async_stop(self, **kwargs):
"""Stop the vacuum cleaner.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(partial(self.stop, **kwargs))
def return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
raise NotImplementedError()
async 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.
"""
await self.hass.async_add_executor_job(
partial(self.return_to_base, **kwargs))
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
raise NotImplementedError()
async def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.clean_spot, **kwargs))
def locate(self, **kwargs):
"""Locate the vacuum cleaner."""
raise NotImplementedError()
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(partial(self.locate, **kwargs))
def set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed."""
raise NotImplementedError()
async def async_set_fan_speed(self, fan_speed, **kwargs):
"""Set fan speed.
This method must be run in the event loop.
"""
await self.hass.async_add_executor_job(
partial(self.set_fan_speed, fan_speed, **kwargs))
def send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner."""
raise NotImplementedError()
async 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.
"""
await self.hass.async_add_executor_job(
partial(self.send_command, command, params=params, **kwargs))
class VacuumDevice(_BaseVacuum, ToggleEntity):
"""Representation of a vacuum cleaner robot."""
@property
def status(self):
"""Return the status 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 state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
@ -267,100 +364,54 @@ class VacuumDevice(ToggleEntity):
"""Turn the vacuum on and start cleaning."""
raise NotImplementedError()
def async_turn_on(self, **kwargs):
async 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.
This method must be run in the event loop.
"""
return self.hass.async_add_job(partial(self.turn_on, **kwargs))
await self.hass.async_add_executor_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):
async 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.
This method must be run in the event loop.
"""
return self.hass.async_add_job(partial(self.turn_off, **kwargs))
await self.hass.async_add_executor_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.
class StateVacuumDevice(_BaseVacuum):
"""Representation of a vacuum cleaner robot that supports states."""
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))
@property
def state(self):
"""Return the state of the vacuum cleaner."""
return None
def stop(self, **kwargs):
"""Stop the vacuum cleaner."""
raise NotImplementedError()
@property
def battery_icon(self):
"""Return the battery icon for the vacuum cleaner."""
charging = bool(self.state == STATE_DOCKED)
def async_stop(self, **kwargs):
"""Stop the vacuum cleaner.
return icon_for_battery_level(
battery_level=self.battery_level, charging=charging)
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(partial(self.stop, **kwargs))
@property
def state_attributes(self):
"""Return the state attributes of the vacuum cleaner."""
data = {}
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
raise NotImplementedError()
if self.battery_level is not None:
data[ATTR_BATTERY_LEVEL] = self.battery_level
data[ATTR_BATTERY_ICON] = self.battery_icon
def async_clean_spot(self, **kwargs):
"""Perform a spot clean-up.
if self.fan_speed is not None:
data[ATTR_FAN_SPEED] = self.fan_speed
data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list
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):
"""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))
return data

View File

@ -10,7 +10,9 @@ from homeassistant.components.vacuum import (
ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, VacuumDevice)
SUPPORT_TURN_ON, SUPPORT_STATE, STATE_CLEANING, STATE_DOCKED,
STATE_IDLE, STATE_PAUSED, STATE_RETURNING, VacuumDevice,
StateVacuumDevice)
_LOGGER = logging.getLogger(__name__)
@ -28,12 +30,17 @@ SUPPORT_ALL_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
SUPPORT_LOCATE | SUPPORT_STATUS | SUPPORT_BATTERY | \
SUPPORT_CLEAN_SPOT
SUPPORT_STATE_SERVICES = SUPPORT_STATE | SUPPORT_PAUSE | SUPPORT_STOP | \
SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | \
SUPPORT_BATTERY | SUPPORT_CLEAN_SPOT
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'
DEMO_VACUUM_NONE = '4_Fourth_floor'
DEMO_VACUUM_STATE = '5_Fifth_floor'
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -44,6 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
DemoVacuum(DEMO_VACUUM_BASIC, SUPPORT_BASIC_SERVICES),
DemoVacuum(DEMO_VACUUM_MINIMAL, SUPPORT_MINIMAL_SERVICES),
DemoVacuum(DEMO_VACUUM_NONE, 0),
StateDemoVacuum(DEMO_VACUUM_STATE),
])
@ -204,3 +212,118 @@ class DemoVacuum(VacuumDevice):
self._status = 'Executing {}({})'.format(command, params)
self._state = True
self.schedule_update_ha_state()
class StateDemoVacuum(StateVacuumDevice):
"""Representation of a demo vacuum supporting states."""
def __init__(self, name):
"""Initialize the vacuum."""
self._name = name
self._supported_features = SUPPORT_STATE_SERVICES
self._state = STATE_DOCKED
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 should_poll(self):
"""No polling needed for a demo vacuum."""
return False
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
@property
def state(self):
"""Return the current state of the vacuum."""
return self._state
@property
def battery_level(self):
"""Return the current battery level of the vacuum."""
if self.supported_features & SUPPORT_BATTERY == 0:
return
return max(0, min(100, self._battery_level))
@property
def fan_speed(self):
"""Return the current fan speed of the vacuum."""
if self.supported_features & SUPPORT_FAN_SPEED == 0:
return
return self._fan_speed
@property
def fan_speed_list(self):
"""Return the list of supported fan speeds."""
if self.supported_features & SUPPORT_FAN_SPEED == 0:
return
return FAN_SPEEDS
@property
def device_state_attributes(self):
"""Return device state attributes."""
return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)}
def start_pause(self, **kwargs):
"""Start, pause or resume the cleaning task."""
if self.supported_features & SUPPORT_PAUSE == 0:
return
if self._state == STATE_CLEANING:
self._state = STATE_PAUSED
else:
self._state = STATE_CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def stop(self, **kwargs):
"""Stop the cleaning task, do not return to dock."""
if self.supported_features & SUPPORT_STOP == 0:
return
self._state = STATE_IDLE
self.schedule_update_ha_state()
def return_to_base(self, **kwargs):
"""Return dock to charging base."""
if self.supported_features & SUPPORT_RETURN_HOME == 0:
return
self._state = STATE_RETURNING
self.schedule_update_ha_state()
self.hass.loop.call_later(30, self.__set_state_to_dock)
def clean_spot(self, **kwargs):
"""Perform a spot clean-up."""
if self.supported_features & SUPPORT_CLEAN_SPOT == 0:
return
self._state = STATE_CLEANING
self._cleaned_area += 1.32
self._battery_level -= 1
self.schedule_update_ha_state()
def set_fan_speed(self, fan_speed, **kwargs):
"""Set the vacuum's fan speed."""
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 __set_state_to_dock(self):
self._state = STATE_DOCKED
self.schedule_update_ha_state()

View File

@ -6,10 +6,12 @@ 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)
SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED,
STATE_DOCKED, STATE_CLEANING, STATE_PAUSED, STATE_IDLE,
STATE_RETURNING)
from homeassistant.components.vacuum.demo import (
DEMO_VACUUM_BASIC, DEMO_VACUUM_COMPLETE, DEMO_VACUUM_MINIMAL,
DEMO_VACUUM_MOST, DEMO_VACUUM_NONE, FAN_SPEEDS)
DEMO_VACUUM_MOST, DEMO_VACUUM_NONE, DEMO_VACUUM_STATE, FAN_SPEEDS)
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON)
from homeassistant.setup import setup_component
@ -21,6 +23,7 @@ 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()
ENTITY_VACUUM_STATE = '{}.{}'.format(DOMAIN, DEMO_VACUUM_STATE).lower()
class TestVacuumDemo(unittest.TestCase):
@ -79,6 +82,14 @@ 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_STATE)
self.assertEqual(5244, state.attributes.get(ATTR_SUPPORTED_FEATURES))
self.assertEqual(STATE_DOCKED, state.state)
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))
def test_methods(self):
"""Test if methods call the services as expected."""
self.hass.states.set(ENTITY_VACUUM_BASIC, STATE_ON)
@ -147,6 +158,41 @@ class TestVacuumDemo(unittest.TestCase):
self.assertIn("spot", state.attributes.get(ATTR_STATUS))
self.assertEqual(STATE_ON, state.state)
vacuum.start_pause(self.hass, ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(STATE_CLEANING, state.state)
vacuum.start_pause(self.hass, ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(STATE_PAUSED, state.state)
vacuum.stop(self.hass, ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(STATE_IDLE, state.state)
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100)
self.assertNotEqual(STATE_DOCKED, state.state)
vacuum.return_to_base(self.hass, ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(STATE_RETURNING, state.state)
vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1],
entity_id=ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED))
vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(STATE_CLEANING, state.state)
def test_unsupported_methods(self):
"""Test service calls for unsupported vacuums."""
self.hass.states.set(ENTITY_VACUUM_NONE, STATE_ON)
@ -201,6 +247,22 @@ class TestVacuumDemo(unittest.TestCase):
self.assertNotIn("spot", state.attributes.get(ATTR_STATUS))
self.assertEqual(STATE_OFF, state.state)
# StateVacuumDevice does not support on/off
vacuum.turn_on(self.hass, entity_id=ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertNotEqual(STATE_CLEANING, state.state)
vacuum.turn_off(self.hass, entity_id=ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertNotEqual(STATE_RETURNING, state.state)
vacuum.toggle(self.hass, entity_id=ENTITY_VACUUM_STATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertNotEqual(STATE_CLEANING, state.state)
def test_services(self):
"""Test vacuum services."""
# Test send_command
@ -241,9 +303,11 @@ class TestVacuumDemo(unittest.TestCase):
def test_set_fan_speed(self):
"""Test vacuum service to set the fan speed."""
group_vacuums = ','.join([ENTITY_VACUUM_BASIC,
ENTITY_VACUUM_COMPLETE])
ENTITY_VACUUM_COMPLETE,
ENTITY_VACUUM_STATE])
old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC)
old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE)
old_state_state = self.hass.states.get(ENTITY_VACUUM_STATE)
vacuum.set_fan_speed(
self.hass, FAN_SPEEDS[0], entity_id=group_vacuums)
@ -251,6 +315,7 @@ class TestVacuumDemo(unittest.TestCase):
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)
new_state_state = self.hass.states.get(ENTITY_VACUUM_STATE)
self.assertEqual(old_state_basic, new_state_basic)
self.assertNotIn(ATTR_FAN_SPEED, new_state_basic.attributes)
@ -261,6 +326,12 @@ class TestVacuumDemo(unittest.TestCase):
self.assertEqual(FAN_SPEEDS[0],
new_state_complete.attributes[ATTR_FAN_SPEED])
self.assertNotEqual(old_state_state, new_state_state)
self.assertEqual(FAN_SPEEDS[1],
old_state_state.attributes[ATTR_FAN_SPEED])
self.assertEqual(FAN_SPEEDS[0],
new_state_state.attributes[ATTR_FAN_SPEED])
def test_send_command(self):
"""Test vacuum service to send a command."""
group_vacuums = ','.join([ENTITY_VACUUM_BASIC,