mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +00:00
Add lawn mower support to Google Assistant (#140530)
* Add lawn mower support to google assistant * Update snapshots * Sort alphabetically * Refactor service call * Refactor service call * Feedback
This commit is contained in:
parent
e740e341c8
commit
324f208d68
@ -14,6 +14,7 @@ from homeassistant.components import (
|
|||||||
input_boolean,
|
input_boolean,
|
||||||
input_button,
|
input_button,
|
||||||
input_select,
|
input_select,
|
||||||
|
lawn_mower,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
media_player,
|
media_player,
|
||||||
@ -58,6 +59,7 @@ DEFAULT_EXPOSED_DOMAINS = [
|
|||||||
"humidifier",
|
"humidifier",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
"input_select",
|
"input_select",
|
||||||
|
"lawn_mower",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
"media_player",
|
"media_player",
|
||||||
@ -88,6 +90,7 @@ TYPE_GATE = f"{PREFIX_TYPES}GATE"
|
|||||||
TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER"
|
TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER"
|
||||||
TYPE_LIGHT = f"{PREFIX_TYPES}LIGHT"
|
TYPE_LIGHT = f"{PREFIX_TYPES}LIGHT"
|
||||||
TYPE_LOCK = f"{PREFIX_TYPES}LOCK"
|
TYPE_LOCK = f"{PREFIX_TYPES}LOCK"
|
||||||
|
TYPE_MOWER = f"{PREFIX_TYPES}MOWER"
|
||||||
TYPE_OUTLET = f"{PREFIX_TYPES}OUTLET"
|
TYPE_OUTLET = f"{PREFIX_TYPES}OUTLET"
|
||||||
TYPE_RECEIVER = f"{PREFIX_TYPES}AUDIO_VIDEO_RECEIVER"
|
TYPE_RECEIVER = f"{PREFIX_TYPES}AUDIO_VIDEO_RECEIVER"
|
||||||
TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
|
TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
|
||||||
@ -149,6 +152,7 @@ DOMAIN_TO_GOOGLE_TYPES = {
|
|||||||
input_boolean.DOMAIN: TYPE_SWITCH,
|
input_boolean.DOMAIN: TYPE_SWITCH,
|
||||||
input_button.DOMAIN: TYPE_SCENE,
|
input_button.DOMAIN: TYPE_SCENE,
|
||||||
input_select.DOMAIN: TYPE_SENSOR,
|
input_select.DOMAIN: TYPE_SENSOR,
|
||||||
|
lawn_mower.DOMAIN: TYPE_MOWER,
|
||||||
light.DOMAIN: TYPE_LIGHT,
|
light.DOMAIN: TYPE_LIGHT,
|
||||||
lock.DOMAIN: TYPE_LOCK,
|
lock.DOMAIN: TYPE_LOCK,
|
||||||
media_player.DOMAIN: TYPE_SETTOP,
|
media_player.DOMAIN: TYPE_SETTOP,
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.components import (
|
|||||||
input_boolean,
|
input_boolean,
|
||||||
input_button,
|
input_button,
|
||||||
input_select,
|
input_select,
|
||||||
|
lawn_mower,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
media_player,
|
media_player,
|
||||||
@ -42,6 +43,7 @@ from homeassistant.components.climate import ClimateEntityFeature
|
|||||||
from homeassistant.components.cover import CoverEntityFeature
|
from homeassistant.components.cover import CoverEntityFeature
|
||||||
from homeassistant.components.fan import FanEntityFeature
|
from homeassistant.components.fan import FanEntityFeature
|
||||||
from homeassistant.components.humidifier import HumidifierEntityFeature
|
from homeassistant.components.humidifier import HumidifierEntityFeature
|
||||||
|
from homeassistant.components.lawn_mower import LawnMowerEntityFeature
|
||||||
from homeassistant.components.light import LightEntityFeature
|
from homeassistant.components.light import LightEntityFeature
|
||||||
from homeassistant.components.lock import LockState
|
from homeassistant.components.lock import LockState
|
||||||
from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType
|
from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType
|
||||||
@ -714,7 +716,7 @@ class DockTrait(_Trait):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def supported(domain, features, device_class, _):
|
def supported(domain, features, device_class, _):
|
||||||
"""Test if state is supported."""
|
"""Test if state is supported."""
|
||||||
return domain == vacuum.DOMAIN
|
return domain in (vacuum.DOMAIN, lawn_mower.DOMAIN)
|
||||||
|
|
||||||
def sync_attributes(self) -> dict[str, Any]:
|
def sync_attributes(self) -> dict[str, Any]:
|
||||||
"""Return dock attributes for a sync request."""
|
"""Return dock attributes for a sync request."""
|
||||||
@ -722,17 +724,32 @@ class DockTrait(_Trait):
|
|||||||
|
|
||||||
def query_attributes(self) -> dict[str, Any]:
|
def query_attributes(self) -> dict[str, Any]:
|
||||||
"""Return dock query attributes."""
|
"""Return dock query attributes."""
|
||||||
return {"isDocked": self.state.state == vacuum.VacuumActivity.DOCKED}
|
domain = self.state.domain
|
||||||
|
state = self.state.state
|
||||||
|
if domain == vacuum.DOMAIN:
|
||||||
|
return {"isDocked": state == vacuum.VacuumActivity.DOCKED}
|
||||||
|
if domain == lawn_mower.DOMAIN:
|
||||||
|
return {"isDocked": state == lawn_mower.LawnMowerActivity.DOCKED}
|
||||||
|
raise NotImplementedError(f"Unsupported domain {domain}")
|
||||||
|
|
||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
"""Execute a dock command."""
|
"""Execute a dock command."""
|
||||||
await self.hass.services.async_call(
|
domain = self.state.domain
|
||||||
self.state.domain,
|
service: str | None = None
|
||||||
vacuum.SERVICE_RETURN_TO_BASE,
|
|
||||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
if domain == vacuum.DOMAIN:
|
||||||
blocking=not self.config.should_report_state,
|
service = vacuum.SERVICE_RETURN_TO_BASE
|
||||||
context=data.context,
|
elif domain == lawn_mower.DOMAIN:
|
||||||
)
|
service = lawn_mower.SERVICE_DOCK
|
||||||
|
|
||||||
|
if service:
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
self.state.domain,
|
||||||
|
service,
|
||||||
|
{ATTR_ENTITY_ID: self.state.entity_id},
|
||||||
|
blocking=not self.config.should_report_state,
|
||||||
|
context=data.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_trait
|
@register_trait
|
||||||
@ -843,7 +860,7 @@ class StartStopTrait(_Trait):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def supported(domain, features, device_class, _):
|
def supported(domain, features, device_class, _):
|
||||||
"""Test if state is supported."""
|
"""Test if state is supported."""
|
||||||
if domain == vacuum.DOMAIN:
|
if domain in (vacuum.DOMAIN, lawn_mower.DOMAIN):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -863,6 +880,12 @@ class StartStopTrait(_Trait):
|
|||||||
& VacuumEntityFeature.PAUSE
|
& VacuumEntityFeature.PAUSE
|
||||||
!= 0
|
!= 0
|
||||||
}
|
}
|
||||||
|
if domain == lawn_mower.DOMAIN:
|
||||||
|
return {
|
||||||
|
"pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
& LawnMowerEntityFeature.PAUSE
|
||||||
|
!= 0
|
||||||
|
}
|
||||||
if domain in COVER_VALVE_DOMAINS:
|
if domain in COVER_VALVE_DOMAINS:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@ -878,6 +901,11 @@ class StartStopTrait(_Trait):
|
|||||||
"isRunning": state == vacuum.VacuumActivity.CLEANING,
|
"isRunning": state == vacuum.VacuumActivity.CLEANING,
|
||||||
"isPaused": state == vacuum.VacuumActivity.PAUSED,
|
"isPaused": state == vacuum.VacuumActivity.PAUSED,
|
||||||
}
|
}
|
||||||
|
if domain == lawn_mower.DOMAIN:
|
||||||
|
return {
|
||||||
|
"isRunning": state == lawn_mower.LawnMowerActivity.MOWING,
|
||||||
|
"isPaused": state == lawn_mower.LawnMowerActivity.PAUSED,
|
||||||
|
}
|
||||||
|
|
||||||
if domain in COVER_VALVE_DOMAINS:
|
if domain in COVER_VALVE_DOMAINS:
|
||||||
return {
|
return {
|
||||||
@ -896,46 +924,52 @@ class StartStopTrait(_Trait):
|
|||||||
if domain == vacuum.DOMAIN:
|
if domain == vacuum.DOMAIN:
|
||||||
await self._execute_vacuum(command, data, params, challenge)
|
await self._execute_vacuum(command, data, params, challenge)
|
||||||
return
|
return
|
||||||
|
if domain == lawn_mower.DOMAIN:
|
||||||
|
await self._execute_lawn_mower(command, data, params, challenge)
|
||||||
|
return
|
||||||
if domain in COVER_VALVE_DOMAINS:
|
if domain in COVER_VALVE_DOMAINS:
|
||||||
await self._execute_cover_or_valve(command, data, params, challenge)
|
await self._execute_cover_or_valve(command, data, params, challenge)
|
||||||
return
|
return
|
||||||
|
|
||||||
async def _execute_vacuum(self, command, data, params, challenge):
|
async def _execute_vacuum(self, command, data, params, challenge):
|
||||||
"""Execute a StartStop command."""
|
"""Execute a StartStop command."""
|
||||||
|
service: str | None = None
|
||||||
if command == COMMAND_START_STOP:
|
if command == COMMAND_START_STOP:
|
||||||
if params["start"]:
|
service = vacuum.SERVICE_START if params["start"] else vacuum.SERVICE_STOP
|
||||||
await self.hass.services.async_call(
|
|
||||||
self.state.domain,
|
|
||||||
vacuum.SERVICE_START,
|
|
||||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
|
||||||
blocking=not self.config.should_report_state,
|
|
||||||
context=data.context,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.hass.services.async_call(
|
|
||||||
self.state.domain,
|
|
||||||
vacuum.SERVICE_STOP,
|
|
||||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
|
||||||
blocking=not self.config.should_report_state,
|
|
||||||
context=data.context,
|
|
||||||
)
|
|
||||||
elif command == COMMAND_PAUSE_UNPAUSE:
|
elif command == COMMAND_PAUSE_UNPAUSE:
|
||||||
if params["pause"]:
|
service = vacuum.SERVICE_PAUSE if params["pause"] else vacuum.SERVICE_START
|
||||||
await self.hass.services.async_call(
|
if service:
|
||||||
self.state.domain,
|
await self.hass.services.async_call(
|
||||||
vacuum.SERVICE_PAUSE,
|
self.state.domain,
|
||||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
service,
|
||||||
blocking=not self.config.should_report_state,
|
{ATTR_ENTITY_ID: self.state.entity_id},
|
||||||
context=data.context,
|
blocking=not self.config.should_report_state,
|
||||||
)
|
context=data.context,
|
||||||
else:
|
)
|
||||||
await self.hass.services.async_call(
|
|
||||||
self.state.domain,
|
async def _execute_lawn_mower(self, command, data, params, challenge):
|
||||||
vacuum.SERVICE_START,
|
"""Execute a StartStop command."""
|
||||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
service: str | None = None
|
||||||
blocking=not self.config.should_report_state,
|
if command == COMMAND_START_STOP:
|
||||||
context=data.context,
|
service = (
|
||||||
)
|
lawn_mower.SERVICE_START_MOWING
|
||||||
|
if params["start"]
|
||||||
|
else lawn_mower.SERVICE_DOCK
|
||||||
|
)
|
||||||
|
elif command == COMMAND_PAUSE_UNPAUSE:
|
||||||
|
service = (
|
||||||
|
lawn_mower.SERVICE_PAUSE
|
||||||
|
if params["pause"]
|
||||||
|
else lawn_mower.SERVICE_START_MOWING
|
||||||
|
)
|
||||||
|
if service:
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
self.state.domain,
|
||||||
|
service,
|
||||||
|
{ATTR_ENTITY_ID: self.state.entity_id},
|
||||||
|
blocking=not self.config.should_report_state,
|
||||||
|
context=data.context,
|
||||||
|
)
|
||||||
|
|
||||||
async def _execute_cover_or_valve(self, command, data, params, challenge):
|
async def _execute_cover_or_valve(self, command, data, params, challenge):
|
||||||
"""Execute a StartStop command."""
|
"""Execute a StartStop command."""
|
||||||
|
@ -98,6 +98,7 @@
|
|||||||
'humidifier',
|
'humidifier',
|
||||||
'input_boolean',
|
'input_boolean',
|
||||||
'input_select',
|
'input_select',
|
||||||
|
'lawn_mower',
|
||||||
'light',
|
'light',
|
||||||
'lock',
|
'lock',
|
||||||
'media_player',
|
'media_player',
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.components import (
|
|||||||
input_boolean,
|
input_boolean,
|
||||||
input_button,
|
input_button,
|
||||||
input_select,
|
input_select,
|
||||||
|
lawn_mower,
|
||||||
light,
|
light,
|
||||||
lock,
|
lock,
|
||||||
media_player,
|
media_player,
|
||||||
@ -44,6 +45,7 @@ from homeassistant.components.fan import FanEntityFeature
|
|||||||
from homeassistant.components.google_assistant import const, error, helpers, trait
|
from homeassistant.components.google_assistant import const, error, helpers, trait
|
||||||
from homeassistant.components.google_assistant.error import SmartHomeError
|
from homeassistant.components.google_assistant.error import SmartHomeError
|
||||||
from homeassistant.components.humidifier import HumidifierEntityFeature
|
from homeassistant.components.humidifier import HumidifierEntityFeature
|
||||||
|
from homeassistant.components.lawn_mower import LawnMowerEntityFeature
|
||||||
from homeassistant.components.light import LightEntityFeature
|
from homeassistant.components.light import LightEntityFeature
|
||||||
from homeassistant.components.lock import LockEntityFeature
|
from homeassistant.components.lock import LockEntityFeature
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
@ -589,6 +591,64 @@ async def test_startstop_vacuum(hass: HomeAssistant) -> None:
|
|||||||
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_dock_lawn_mower(hass: HomeAssistant) -> None:
|
||||||
|
"""Test dock trait support for lawn mower domain."""
|
||||||
|
assert helpers.get_google_type(lawn_mower.DOMAIN, None) is not None
|
||||||
|
assert trait.DockTrait.supported(lawn_mower.DOMAIN, 0, None, None)
|
||||||
|
|
||||||
|
trt = trait.DockTrait(
|
||||||
|
hass, State("lawn_mower.bla", lawn_mower.LawnMowerActivity.MOWING), BASIC_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trt.sync_attributes() == {}
|
||||||
|
|
||||||
|
assert trt.query_attributes() == {"isDocked": False}
|
||||||
|
|
||||||
|
calls = async_mock_service(hass, lawn_mower.DOMAIN, lawn_mower.SERVICE_DOCK)
|
||||||
|
await trt.execute(trait.COMMAND_DOCK, BASIC_DATA, {}, {})
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data == {ATTR_ENTITY_ID: "lawn_mower.bla"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_startstop_lawn_mower(hass: HomeAssistant) -> None:
|
||||||
|
"""Test startStop trait support for lawn mower domain."""
|
||||||
|
assert helpers.get_google_type(lawn_mower.DOMAIN, None) is not None
|
||||||
|
assert trait.StartStopTrait.supported(lawn_mower.DOMAIN, 0, None, None)
|
||||||
|
|
||||||
|
trt = trait.StartStopTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"lawn_mower.bla",
|
||||||
|
lawn_mower.LawnMowerActivity.PAUSED,
|
||||||
|
{ATTR_SUPPORTED_FEATURES: LawnMowerEntityFeature.PAUSE},
|
||||||
|
),
|
||||||
|
BASIC_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert trt.sync_attributes() == {"pausable": True}
|
||||||
|
|
||||||
|
assert trt.query_attributes() == {"isRunning": False, "isPaused": True}
|
||||||
|
|
||||||
|
start_calls = async_mock_service(
|
||||||
|
hass, lawn_mower.DOMAIN, lawn_mower.SERVICE_START_MOWING
|
||||||
|
)
|
||||||
|
await trt.execute(trait.COMMAND_START_STOP, BASIC_DATA, {"start": True}, {})
|
||||||
|
assert len(start_calls) == 1
|
||||||
|
assert start_calls[0].data == {ATTR_ENTITY_ID: "lawn_mower.bla"}
|
||||||
|
|
||||||
|
pause_calls = async_mock_service(hass, lawn_mower.DOMAIN, lawn_mower.SERVICE_PAUSE)
|
||||||
|
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"pause": True}, {})
|
||||||
|
assert len(pause_calls) == 1
|
||||||
|
assert pause_calls[0].data == {ATTR_ENTITY_ID: "lawn_mower.bla"}
|
||||||
|
|
||||||
|
unpause_calls = async_mock_service(
|
||||||
|
hass, lawn_mower.DOMAIN, lawn_mower.SERVICE_START_MOWING
|
||||||
|
)
|
||||||
|
await trt.execute(trait.COMMAND_PAUSE_UNPAUSE, BASIC_DATA, {"pause": False}, {})
|
||||||
|
assert len(unpause_calls) == 1
|
||||||
|
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "lawn_mower.bla"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
(
|
(
|
||||||
"domain",
|
"domain",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user