Support Next/Previous for InputSelector (#38378)

* Support Next/Previous for InputSelector

* Update homeassistant/components/google_assistant/trait.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Adjust to match new version of _next_selected

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Joakim Plate 2020-08-06 15:58:26 +02:00 committed by GitHub
parent aaad986002
commit 39e6bca682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 116 additions and 2 deletions

View File

@ -97,6 +97,7 @@ ERR_NOT_SUPPORTED = "notSupported"
ERR_PROTOCOL_ERROR = "protocolError"
ERR_UNKNOWN_ERROR = "unknownError"
ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported"
ERR_UNSUPPORTED_INPUT = "unsupportedInput"
ERR_ALREADY_DISARMED = "alreadyDisarmed"
ERR_ALREADY_ARMED = "alreadyArmed"

View File

@ -1,5 +1,6 @@
"""Implement the Google Smart Home traits."""
import logging
from typing import List, Optional
from homeassistant.components import (
alarm_control_panel,
@ -68,6 +69,7 @@ from .const import (
ERR_CHALLENGE_NOT_SETUP,
ERR_FUNCTION_NOT_SUPPORTED,
ERR_NOT_SUPPORTED,
ERR_UNSUPPORTED_INPUT,
ERR_VALUE_OUT_OF_RANGE,
)
from .error import ChallengeNeeded, SmartHomeError
@ -114,6 +116,8 @@ COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput"
COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose"
COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
@ -145,6 +149,20 @@ def _google_temp_unit(units):
return "C"
def _next_selected(items: List[str], selected: Optional[str]) -> Optional[str]:
"""Return the next item in a item list starting at given value.
If selected is missing in items, None is returned
"""
try:
index = items.index(selected)
except ValueError:
return None
next_item = 0 if index == len(items) - 1 else index + 1
return items[next_item]
class _Trait:
"""Represents a Trait inside Google Assistant skill."""
@ -1395,7 +1413,7 @@ class InputSelectorTrait(_Trait):
"""
name = TRAIT_INPUTSELECTOR
commands = [COMMAND_INPUT]
commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT]
SYNONYMS = {}
@ -1428,7 +1446,20 @@ class InputSelectorTrait(_Trait):
async def execute(self, command, data, params, challenge):
"""Execute an SetInputSource command."""
requested_source = params.get("newInput")
sources = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
source = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE)
if command == COMMAND_INPUT:
requested_source = params.get("newInput")
elif command == COMMAND_NEXT_INPUT:
requested_source = _next_selected(sources, source)
elif command == COMMAND_PREVIOUS_INPUT:
requested_source = _next_selected(list(reversed(sources)), source)
else:
raise SmartHomeError(ERR_NOT_SUPPORTED, "Unsupported command")
if requested_source not in sources:
raise SmartHomeError(ERR_UNSUPPORTED_INPUT, "Unsupported input")
await self.hass.services.async_call(
media_player.DOMAIN,

View File

@ -24,6 +24,7 @@ from homeassistant.components import (
)
from homeassistant.components.climate import const as climate
from homeassistant.components.google_assistant import const, error, helpers, trait
from homeassistant.components.google_assistant.error import SmartHomeError
from homeassistant.components.humidifier import const as humidifier
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
@ -1428,6 +1429,87 @@ async def test_inputselector(hass):
assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"}
@pytest.mark.parametrize(
"sources,source,source_next,source_prev",
[
(["a"], "a", "a", "a"),
(["a", "b"], "a", "b", "b"),
(["a", "b", "c"], "a", "b", "c"),
],
)
async def test_inputselector_nextprev(hass, sources, source, source_next, source_prev):
"""Test input selector trait."""
trt = trait.InputSelectorTrait(
hass,
State(
"media_player.living_room",
media_player.STATE_PLAYING,
attributes={
media_player.ATTR_INPUT_SOURCE_LIST: sources,
media_player.ATTR_INPUT_SOURCE: source,
},
),
BASIC_CONFIG,
)
assert trt.can_execute("action.devices.commands.NextInput", params={})
assert trt.can_execute("action.devices.commands.PreviousInput", params={})
calls = async_mock_service(
hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE
)
await trt.execute(
"action.devices.commands.NextInput", BASIC_DATA, {}, {},
)
await trt.execute(
"action.devices.commands.PreviousInput", BASIC_DATA, {}, {},
)
assert len(calls) == 2
assert calls[0].data == {
"entity_id": "media_player.living_room",
"source": source_next,
}
assert calls[1].data == {
"entity_id": "media_player.living_room",
"source": source_prev,
}
@pytest.mark.parametrize(
"sources,source", [(None, "a"), (["a", "b"], None), (["a", "b"], "c")]
)
async def test_inputselector_nextprev_invalid(hass, sources, source):
"""Test input selector trait."""
trt = trait.InputSelectorTrait(
hass,
State(
"media_player.living_room",
media_player.STATE_PLAYING,
attributes={
media_player.ATTR_INPUT_SOURCE_LIST: sources,
media_player.ATTR_INPUT_SOURCE: source,
},
),
BASIC_CONFIG,
)
with pytest.raises(SmartHomeError):
await trt.execute(
"action.devices.commands.NextInput", BASIC_DATA, {}, {},
)
with pytest.raises(SmartHomeError):
await trt.execute(
"action.devices.commands.PreviousInput", BASIC_DATA, {}, {},
)
with pytest.raises(SmartHomeError):
await trt.execute(
"action.devices.commands.InvalidCommand", BASIC_DATA, {}, {},
)
async def test_modes_input_select(hass):
"""Test Input Select Mode trait."""
assert helpers.get_google_type(input_select.DOMAIN, None) is not None