mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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:
parent
aaad986002
commit
39e6bca682
@ -97,6 +97,7 @@ ERR_NOT_SUPPORTED = "notSupported"
|
|||||||
ERR_PROTOCOL_ERROR = "protocolError"
|
ERR_PROTOCOL_ERROR = "protocolError"
|
||||||
ERR_UNKNOWN_ERROR = "unknownError"
|
ERR_UNKNOWN_ERROR = "unknownError"
|
||||||
ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported"
|
ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported"
|
||||||
|
ERR_UNSUPPORTED_INPUT = "unsupportedInput"
|
||||||
|
|
||||||
ERR_ALREADY_DISARMED = "alreadyDisarmed"
|
ERR_ALREADY_DISARMED = "alreadyDisarmed"
|
||||||
ERR_ALREADY_ARMED = "alreadyArmed"
|
ERR_ALREADY_ARMED = "alreadyArmed"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Implement the Google Smart Home traits."""
|
"""Implement the Google Smart Home traits."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
alarm_control_panel,
|
alarm_control_panel,
|
||||||
@ -68,6 +69,7 @@ from .const import (
|
|||||||
ERR_CHALLENGE_NOT_SETUP,
|
ERR_CHALLENGE_NOT_SETUP,
|
||||||
ERR_FUNCTION_NOT_SUPPORTED,
|
ERR_FUNCTION_NOT_SUPPORTED,
|
||||||
ERR_NOT_SUPPORTED,
|
ERR_NOT_SUPPORTED,
|
||||||
|
ERR_UNSUPPORTED_INPUT,
|
||||||
ERR_VALUE_OUT_OF_RANGE,
|
ERR_VALUE_OUT_OF_RANGE,
|
||||||
)
|
)
|
||||||
from .error import ChallengeNeeded, SmartHomeError
|
from .error import ChallengeNeeded, SmartHomeError
|
||||||
@ -114,6 +116,8 @@ COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock"
|
|||||||
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
|
COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed"
|
||||||
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
|
COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes"
|
||||||
COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
|
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_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose"
|
||||||
COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
|
COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
|
||||||
COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
|
COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
|
||||||
@ -145,6 +149,20 @@ def _google_temp_unit(units):
|
|||||||
return "C"
|
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:
|
class _Trait:
|
||||||
"""Represents a Trait inside Google Assistant skill."""
|
"""Represents a Trait inside Google Assistant skill."""
|
||||||
|
|
||||||
@ -1395,7 +1413,7 @@ class InputSelectorTrait(_Trait):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = TRAIT_INPUTSELECTOR
|
name = TRAIT_INPUTSELECTOR
|
||||||
commands = [COMMAND_INPUT]
|
commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT]
|
||||||
|
|
||||||
SYNONYMS = {}
|
SYNONYMS = {}
|
||||||
|
|
||||||
@ -1428,7 +1446,20 @@ class InputSelectorTrait(_Trait):
|
|||||||
|
|
||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
"""Execute an SetInputSource command."""
|
"""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(
|
await self.hass.services.async_call(
|
||||||
media_player.DOMAIN,
|
media_player.DOMAIN,
|
||||||
|
@ -24,6 +24,7 @@ from homeassistant.components import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
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.humidifier import const as humidifier
|
from homeassistant.components.humidifier import const as humidifier
|
||||||
from homeassistant.config import async_process_ha_core_config
|
from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.const import (
|
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"}
|
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):
|
async def test_modes_input_select(hass):
|
||||||
"""Test Input Select Mode trait."""
|
"""Test Input Select Mode trait."""
|
||||||
assert helpers.get_google_type(input_select.DOMAIN, None) is not None
|
assert helpers.get_google_type(input_select.DOMAIN, None) is not None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user