mirror of
https://github.com/home-assistant/core.git
synced 2025-07-07 13:27:09 +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_button,
|
||||
input_select,
|
||||
lawn_mower,
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
@ -58,6 +59,7 @@ DEFAULT_EXPOSED_DOMAINS = [
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_select",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
@ -88,6 +90,7 @@ TYPE_GATE = f"{PREFIX_TYPES}GATE"
|
||||
TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER"
|
||||
TYPE_LIGHT = f"{PREFIX_TYPES}LIGHT"
|
||||
TYPE_LOCK = f"{PREFIX_TYPES}LOCK"
|
||||
TYPE_MOWER = f"{PREFIX_TYPES}MOWER"
|
||||
TYPE_OUTLET = f"{PREFIX_TYPES}OUTLET"
|
||||
TYPE_RECEIVER = f"{PREFIX_TYPES}AUDIO_VIDEO_RECEIVER"
|
||||
TYPE_SCENE = f"{PREFIX_TYPES}SCENE"
|
||||
@ -149,6 +152,7 @@ DOMAIN_TO_GOOGLE_TYPES = {
|
||||
input_boolean.DOMAIN: TYPE_SWITCH,
|
||||
input_button.DOMAIN: TYPE_SCENE,
|
||||
input_select.DOMAIN: TYPE_SENSOR,
|
||||
lawn_mower.DOMAIN: TYPE_MOWER,
|
||||
light.DOMAIN: TYPE_LIGHT,
|
||||
lock.DOMAIN: TYPE_LOCK,
|
||||
media_player.DOMAIN: TYPE_SETTOP,
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.components import (
|
||||
input_boolean,
|
||||
input_button,
|
||||
input_select,
|
||||
lawn_mower,
|
||||
light,
|
||||
lock,
|
||||
media_player,
|
||||
@ -42,6 +43,7 @@ from homeassistant.components.climate import ClimateEntityFeature
|
||||
from homeassistant.components.cover import CoverEntityFeature
|
||||
from homeassistant.components.fan import FanEntityFeature
|
||||
from homeassistant.components.humidifier import HumidifierEntityFeature
|
||||
from homeassistant.components.lawn_mower import LawnMowerEntityFeature
|
||||
from homeassistant.components.light import LightEntityFeature
|
||||
from homeassistant.components.lock import LockState
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType
|
||||
@ -714,7 +716,7 @@ class DockTrait(_Trait):
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class, _):
|
||||
"""Test if state is supported."""
|
||||
return domain == vacuum.DOMAIN
|
||||
return domain in (vacuum.DOMAIN, lawn_mower.DOMAIN)
|
||||
|
||||
def sync_attributes(self) -> dict[str, Any]:
|
||||
"""Return dock attributes for a sync request."""
|
||||
@ -722,17 +724,32 @@ class DockTrait(_Trait):
|
||||
|
||||
def query_attributes(self) -> dict[str, Any]:
|
||||
"""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):
|
||||
"""Execute a dock command."""
|
||||
await self.hass.services.async_call(
|
||||
self.state.domain,
|
||||
vacuum.SERVICE_RETURN_TO_BASE,
|
||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
||||
blocking=not self.config.should_report_state,
|
||||
context=data.context,
|
||||
)
|
||||
domain = self.state.domain
|
||||
service: str | None = None
|
||||
|
||||
if domain == vacuum.DOMAIN:
|
||||
service = vacuum.SERVICE_RETURN_TO_BASE
|
||||
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
|
||||
@ -843,7 +860,7 @@ class StartStopTrait(_Trait):
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class, _):
|
||||
"""Test if state is supported."""
|
||||
if domain == vacuum.DOMAIN:
|
||||
if domain in (vacuum.DOMAIN, lawn_mower.DOMAIN):
|
||||
return True
|
||||
|
||||
if (
|
||||
@ -863,6 +880,12 @@ class StartStopTrait(_Trait):
|
||||
& VacuumEntityFeature.PAUSE
|
||||
!= 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:
|
||||
return {}
|
||||
|
||||
@ -878,6 +901,11 @@ class StartStopTrait(_Trait):
|
||||
"isRunning": state == vacuum.VacuumActivity.CLEANING,
|
||||
"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:
|
||||
return {
|
||||
@ -896,46 +924,52 @@ class StartStopTrait(_Trait):
|
||||
if domain == vacuum.DOMAIN:
|
||||
await self._execute_vacuum(command, data, params, challenge)
|
||||
return
|
||||
if domain == lawn_mower.DOMAIN:
|
||||
await self._execute_lawn_mower(command, data, params, challenge)
|
||||
return
|
||||
if domain in COVER_VALVE_DOMAINS:
|
||||
await self._execute_cover_or_valve(command, data, params, challenge)
|
||||
return
|
||||
|
||||
async def _execute_vacuum(self, command, data, params, challenge):
|
||||
"""Execute a StartStop command."""
|
||||
service: str | None = None
|
||||
if command == COMMAND_START_STOP:
|
||||
if params["start"]:
|
||||
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,
|
||||
)
|
||||
service = vacuum.SERVICE_START if params["start"] else vacuum.SERVICE_STOP
|
||||
elif command == COMMAND_PAUSE_UNPAUSE:
|
||||
if params["pause"]:
|
||||
await self.hass.services.async_call(
|
||||
self.state.domain,
|
||||
vacuum.SERVICE_PAUSE,
|
||||
{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_START,
|
||||
{ATTR_ENTITY_ID: self.state.entity_id},
|
||||
blocking=not self.config.should_report_state,
|
||||
context=data.context,
|
||||
)
|
||||
service = vacuum.SERVICE_PAUSE if params["pause"] else vacuum.SERVICE_START
|
||||
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_lawn_mower(self, command, data, params, challenge):
|
||||
"""Execute a StartStop command."""
|
||||
service: str | None = None
|
||||
if command == COMMAND_START_STOP:
|
||||
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):
|
||||
"""Execute a StartStop command."""
|
||||
|
@ -98,6 +98,7 @@
|
||||
'humidifier',
|
||||
'input_boolean',
|
||||
'input_select',
|
||||
'lawn_mower',
|
||||
'light',
|
||||
'lock',
|
||||
'media_player',
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.components import (
|
||||
input_boolean,
|
||||
input_button,
|
||||
input_select,
|
||||
lawn_mower,
|
||||
light,
|
||||
lock,
|
||||
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.error import SmartHomeError
|
||||
from homeassistant.components.humidifier import HumidifierEntityFeature
|
||||
from homeassistant.components.lawn_mower import LawnMowerEntityFeature
|
||||
from homeassistant.components.light import LightEntityFeature
|
||||
from homeassistant.components.lock import LockEntityFeature
|
||||
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"}
|
||||
|
||||
|
||||
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(
|
||||
(
|
||||
"domain",
|
||||
|
Loading…
x
Reference in New Issue
Block a user