mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add TV channel trait to google assistant (#49676)
This commit is contained in:
parent
7b5e63132c
commit
fb61ef500c
@ -99,6 +99,7 @@ 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_UNSUPPORTED_INPUT = "unsupportedInput"
|
||||||
|
ERR_NO_AVAILABLE_CHANNEL = "noAvailableChannel"
|
||||||
|
|
||||||
ERR_ALREADY_DISARMED = "alreadyDisarmed"
|
ERR_ALREADY_DISARMED = "alreadyDisarmed"
|
||||||
ERR_ALREADY_ARMED = "alreadyArmed"
|
ERR_ALREADY_ARMED = "alreadyArmed"
|
||||||
|
@ -23,6 +23,7 @@ from homeassistant.components import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.components.humidifier import const as humidifier
|
from homeassistant.components.humidifier import const as humidifier
|
||||||
|
from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
ATTR_CODE,
|
ATTR_CODE,
|
||||||
@ -71,6 +72,7 @@ from .const import (
|
|||||||
ERR_ALREADY_DISARMED,
|
ERR_ALREADY_DISARMED,
|
||||||
ERR_ALREADY_STOPPED,
|
ERR_ALREADY_STOPPED,
|
||||||
ERR_CHALLENGE_NOT_SETUP,
|
ERR_CHALLENGE_NOT_SETUP,
|
||||||
|
ERR_NO_AVAILABLE_CHANNEL,
|
||||||
ERR_NOT_SUPPORTED,
|
ERR_NOT_SUPPORTED,
|
||||||
ERR_UNSUPPORTED_INPUT,
|
ERR_UNSUPPORTED_INPUT,
|
||||||
ERR_VALUE_OUT_OF_RANGE,
|
ERR_VALUE_OUT_OF_RANGE,
|
||||||
@ -99,6 +101,7 @@ TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm"
|
|||||||
TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting"
|
TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting"
|
||||||
TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl"
|
TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl"
|
||||||
TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState"
|
TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState"
|
||||||
|
TRAIT_CHANNEL = f"{PREFIX_TRAITS}Channel"
|
||||||
|
|
||||||
PREFIX_COMMANDS = "action.devices.commands."
|
PREFIX_COMMANDS = "action.devices.commands."
|
||||||
COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff"
|
COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff"
|
||||||
@ -137,7 +140,7 @@ COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition"
|
|||||||
COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle"
|
COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle"
|
||||||
COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop"
|
COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop"
|
||||||
COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity"
|
COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity"
|
||||||
|
COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel"
|
||||||
|
|
||||||
TRAITS = []
|
TRAITS = []
|
||||||
|
|
||||||
@ -2070,3 +2073,59 @@ class MediaStateTrait(_Trait):
|
|||||||
"activityState": self.activity_lookup.get(self.state.state, "INACTIVE"),
|
"activityState": self.activity_lookup.get(self.state.state, "INACTIVE"),
|
||||||
"playbackState": self.playback_lookup.get(self.state.state, "STOPPED"),
|
"playbackState": self.playback_lookup.get(self.state.state, "STOPPED"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_trait
|
||||||
|
class ChannelTrait(_Trait):
|
||||||
|
"""Trait to get media playback state.
|
||||||
|
|
||||||
|
https://developers.google.com/actions/smarthome/traits/channel
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = TRAIT_CHANNEL
|
||||||
|
commands = [COMMAND_SELECT_CHANNEL]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supported(domain, features, device_class, _):
|
||||||
|
"""Test if state is supported."""
|
||||||
|
if (
|
||||||
|
domain == media_player.DOMAIN
|
||||||
|
and (features & media_player.SUPPORT_PLAY_MEDIA)
|
||||||
|
and device_class == media_player.DEVICE_CLASS_TV
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def sync_attributes(self):
|
||||||
|
"""Return attributes for a sync request."""
|
||||||
|
return {"availableChannels": [], "commandOnlyChannels": True}
|
||||||
|
|
||||||
|
def query_attributes(self):
|
||||||
|
"""Return channel query attributes."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def execute(self, command, data, params, challenge):
|
||||||
|
"""Execute an setChannel command."""
|
||||||
|
if command == COMMAND_SELECT_CHANNEL:
|
||||||
|
channel_number = params.get("channelNumber")
|
||||||
|
else:
|
||||||
|
raise SmartHomeError(ERR_NOT_SUPPORTED, "Unsupported command")
|
||||||
|
|
||||||
|
if not channel_number:
|
||||||
|
raise SmartHomeError(
|
||||||
|
ERR_NO_AVAILABLE_CHANNEL,
|
||||||
|
"Channel is not available",
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
media_player.DOMAIN,
|
||||||
|
media_player.SERVICE_PLAY_MEDIA,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: self.state.entity_id,
|
||||||
|
media_player.ATTR_MEDIA_CONTENT_ID: channel_number,
|
||||||
|
media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
context=data.context,
|
||||||
|
)
|
||||||
|
@ -26,6 +26,10 @@ 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.google_assistant.error import SmartHomeError
|
||||||
from homeassistant.components.humidifier import const as humidifier
|
from homeassistant.components.humidifier import const as humidifier
|
||||||
|
from homeassistant.components.media_player.const import (
|
||||||
|
MEDIA_TYPE_CHANNEL,
|
||||||
|
SERVICE_PLAY_MEDIA,
|
||||||
|
)
|
||||||
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 (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
@ -2653,3 +2657,52 @@ async def test_media_state(hass, state):
|
|||||||
"activityState": trt.activity_lookup.get(state),
|
"activityState": trt.activity_lookup.get(state),
|
||||||
"playbackState": trt.playback_lookup.get(state),
|
"playbackState": trt.playback_lookup.get(state),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_channel(hass):
|
||||||
|
"""Test Channel trait support."""
|
||||||
|
assert helpers.get_google_type(media_player.DOMAIN, None) is not None
|
||||||
|
assert trait.ChannelTrait.supported(
|
||||||
|
media_player.DOMAIN,
|
||||||
|
media_player.SUPPORT_PLAY_MEDIA,
|
||||||
|
media_player.DEVICE_CLASS_TV,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
trait.ChannelTrait.supported(
|
||||||
|
media_player.DOMAIN, media_player.SUPPORT_PLAY_MEDIA, None, None
|
||||||
|
)
|
||||||
|
is False
|
||||||
|
)
|
||||||
|
assert trait.ChannelTrait.supported(media_player.DOMAIN, 0, None, None) is False
|
||||||
|
|
||||||
|
trt = trait.ChannelTrait(hass, State("media_player.demo", STATE_ON), BASIC_CONFIG)
|
||||||
|
|
||||||
|
assert trt.sync_attributes() == {
|
||||||
|
"availableChannels": [],
|
||||||
|
"commandOnlyChannels": True,
|
||||||
|
}
|
||||||
|
assert trt.query_attributes() == {}
|
||||||
|
|
||||||
|
media_player_calls = async_mock_service(
|
||||||
|
hass, media_player.DOMAIN, SERVICE_PLAY_MEDIA
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelNumber": "1"}, {}
|
||||||
|
)
|
||||||
|
assert len(media_player_calls) == 1
|
||||||
|
assert media_player_calls[0].data == {
|
||||||
|
ATTR_ENTITY_ID: "media_player.demo",
|
||||||
|
media_player.ATTR_MEDIA_CONTENT_ID: "1",
|
||||||
|
media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL,
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(SmartHomeError, match="Channel is not available"):
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelCode": "Channel 3"}, {}
|
||||||
|
)
|
||||||
|
assert len(media_player_calls) == 1
|
||||||
|
|
||||||
|
with pytest.raises(SmartHomeError, match="Unsupported command"):
|
||||||
|
await trt.execute("Unknown command", BASIC_DATA, {"channelNumber": "1"}, {})
|
||||||
|
assert len(media_player_calls) == 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user