From f522c6c8c774547d1454476a576f875a0ee59819 Mon Sep 17 00:00:00 2001 From: Jonas Thuresson Date: Mon, 8 Jun 2020 14:57:47 +0200 Subject: [PATCH] Add Xiaomi miio vaccum goto service (#35737) Co-authored-by: Martin Hjelmare --- homeassistant/components/xiaomi_miio/const.py | 1 + .../components/xiaomi_miio/services.yaml | 13 ++ .../components/xiaomi_miio/vacuum.py | 182 ++++++++---------- tests/components/xiaomi_miio/test_vacuum.py | 50 ++++- 4 files changed, 139 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 77f398aa3ad..370244d3015 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -49,6 +49,7 @@ SERVICE_MOVE_REMOTE_CONTROL_STEP = "vacuum_remote_control_move_step" SERVICE_START_REMOTE_CONTROL = "vacuum_remote_control_start" SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" SERVICE_CLEAN_ZONE = "vacuum_clean_zone" +SERVICE_GOTO = "vacuum_goto" # AirQuality Model MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index a92e46f11a1..8883efc8a9b 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -330,3 +330,16 @@ vacuum_clean_zone: repeats: description: Number of cleaning repeats for each zone between 1 and 3. example: "1" + +vacuum_goto: + description: Go to the specified coordinates. + fields: + entity_id: + description: Name of the vacuum entity. + example: "vacuum.xiaomi_vacuum_cleaner" + x_coord: + description: x-coordinate. + example: 27500 + y_coord: + description: y-coordinate. + example: 32000 diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index f37c22a38aa..ef07b5f4741 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -1,5 +1,4 @@ """Support for the Xiaomi vacuum cleaner robot.""" -import asyncio from functools import partial import logging @@ -27,19 +26,12 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - CONF_TOKEN, - STATE_OFF, - STATE_ON, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON +from homeassistant.helpers import config_validation as cv, entity_platform from .const import ( - DOMAIN, SERVICE_CLEAN_ZONE, + SERVICE_GOTO, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, @@ -81,69 +73,6 @@ ATTR_STATUS = "status" ATTR_ZONE_ARRAY = "zone" ATTR_ZONE_REPEATER = "repeats" -VACUUM_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -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_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_ZONE_ARRAY): vol.All( - list, - [ - vol.ExactSequence( - [vol.Coerce(int), vol.Coerce(int), vol.Coerce(int), vol.Coerce(int)] - ) - ], - ), - vol.Required(ATTR_ZONE_REPEATER): vol.All( - vol.Coerce(int), vol.Clamp(min=1, max=3) - ), - } -) - -SERVICE_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_ZONE_ARRAY): vol.All( - list, - [ - vol.ExactSequence( - [vol.Coerce(int), vol.Coerce(int), vol.Coerce(int), vol.Coerce(int)] - ) - ], - ), - vol.Required(ATTR_ZONE_REPEATER): vol.All( - vol.Coerce(int), vol.Clamp(min=1, max=3) - ), - } -) - -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, - }, - SERVICE_CLEAN_ZONE: { - "method": "async_clean_zone", - "schema": SERVICE_SCHEMA_CLEAN_ZONE, - }, -} - SUPPORT_XIAOMI = ( SUPPORT_STATE | SUPPORT_PAUSE @@ -194,39 +123,79 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([mirobo], update_before_add=True) - async def async_service_handler(service): - """Map services to methods on MiroboVacuum.""" - method = SERVICE_TO_METHOD.get(service.service) - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = service.data.get(ATTR_ENTITY_ID) + platform = entity_platform.current_platform.get() - if entity_ids: - target_vacuums = [ - vac - for vac in hass.data[DATA_KEY].values() - if vac.entity_id in entity_ids - ] - else: - target_vacuums = hass.data[DATA_KEY].values() + platform.async_register_entity_service( + SERVICE_START_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_start.__name__, + ) - update_tasks = [] - for vacuum in target_vacuums: - await getattr(vacuum, method["method"])(**params) + platform.async_register_entity_service( + SERVICE_STOP_REMOTE_CONTROL, + {}, + MiroboVacuum.async_remote_control_stop.__name__, + ) - for vacuum in target_vacuums: - update_coro = vacuum.async_update_ha_state(True) - update_tasks.append(update_coro) + platform.async_register_entity_service( + SERVICE_MOVE_REMOTE_CONTROL, + { + 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, + }, + MiroboVacuum.async_remote_control_move.__name__, + ) - if update_tasks: - await asyncio.wait(update_tasks) + platform.async_register_entity_service( + SERVICE_MOVE_REMOTE_CONTROL_STEP, + { + 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, + }, + MiroboVacuum.async_remote_control_move_step.__name__, + ) - 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, schema=schema - ) + platform.async_register_entity_service( + SERVICE_CLEAN_ZONE, + { + vol.Required(ATTR_ZONE_ARRAY): vol.All( + list, + [ + vol.ExactSequence( + [ + vol.Coerce(int), + vol.Coerce(int), + vol.Coerce(int), + vol.Coerce(int), + ] + ) + ], + ), + vol.Required(ATTR_ZONE_REPEATER): vol.All( + vol.Coerce(int), vol.Clamp(min=1, max=3) + ), + }, + MiroboVacuum.async_clean_zone.__name__, + ) + + platform.async_register_entity_service( + SERVICE_GOTO, + { + vol.Required("x_coord"): vol.Coerce(int), + vol.Required("y_coord"): vol.Coerce(int), + }, + MiroboVacuum.async_goto.__name__, + ) class MiroboVacuum(StateVacuumEntity): @@ -450,6 +419,15 @@ class MiroboVacuum(StateVacuumEntity): duration=duration, ) + async def async_goto(self, x_coord: int, y_coord: int): + """Goto the specified coordinates.""" + await self._try_command( + "Unable to send the vacuum cleaner to the specified coordinates: %s", + self._vacuum.goto, + x_coord=x_coord, + y_coord=y_coord, + ) + def update(self): """Fetch state from the device.""" try: diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index d497aec0dca..3949c548844 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -19,6 +19,7 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_ERROR, ) +from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, ATTR_CLEANED_TOTAL_AREA, @@ -35,8 +36,8 @@ from homeassistant.components.xiaomi_miio.vacuum import ( CONF_HOST, CONF_NAME, CONF_TOKEN, - DOMAIN as XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, + SERVICE_GOTO, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, @@ -355,7 +356,10 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"duration": 1000, "rotation": -40, "velocity": -0.1} await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True + XIAOMI_DOMAIN, + SERVICE_MOVE_REMOTE_CONTROL, + {**control, ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.manual_control.assert_has_calls( [mock.call(**control)], any_order=True @@ -364,7 +368,10 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True + XIAOMI_DOMAIN, + SERVICE_STOP_REMOTE_CONTROL, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) @@ -372,7 +379,10 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True + XIAOMI_DOMAIN, + SERVICE_MOVE_REMOTE_CONTROL_STEP, + {**control_once, ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.manual_control_once.assert_has_calls( [mock.call(**control_once)], any_order=True @@ -382,7 +392,10 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"zone": [[123, 123, 123, 123]], "repeats": 2} await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True + XIAOMI_DOMAIN, + SERVICE_CLEAN_ZONE, + {**control, ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.zoned_clean.assert_has_calls( [mock.call([[123, 123, 123, 123, 2]])], any_order=True @@ -453,3 +466,30 @@ async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds): blocking=True, ) assert "ERROR" in caplog.text + + +async def test_xiaomi_vacuum_goto_service(hass, caplog, mock_mirobo_is_on): + """Test vacuum supported features.""" + entity_name = "test_vacuum_cleaner_2" + entity_id = f"{DOMAIN}.{entity_name}" + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_PLATFORM: PLATFORM, + CONF_HOST: "192.168.1.100", + CONF_NAME: entity_name, + CONF_TOKEN: "12345678901234567890123456789012", + } + }, + ) + await hass.async_block_till_done() + + data = {"entity_id": entity_id, "x_coord": 25500, "y_coord": 25500} + await hass.services.async_call(XIAOMI_DOMAIN, SERVICE_GOTO, data, blocking=True) + mock_mirobo_is_on.goto.assert_has_calls( + [mock.call(x_coord=data["x_coord"], y_coord=data["y_coord"])], any_order=True + ) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True)