mirror of
https://github.com/home-assistant/core.git
synced 2026-08-22 06:11:36 +01:00
377 lines
12 KiB
Python
377 lines
12 KiB
Python
"""Tests for the Android TV Remote remote platform."""
|
|
|
|
from unittest.mock import MagicMock, call
|
|
|
|
from androidtvremote2 import ConnectionClosed
|
|
import pytest
|
|
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
REMOTE_ENTITY = "remote.my_android_tv"
|
|
|
|
|
|
async def test_remote_receives_push_updates(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test the Android TV Remote receives push updates and state is updated."""
|
|
new_options = {"apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}}}
|
|
mock_config_entry.add_to_hass(hass)
|
|
hass.config_entries.async_update_entry(mock_config_entry, options=new_options)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
mock_api._on_is_on_updated(False)
|
|
assert hass.states.is_state(REMOTE_ENTITY, STATE_OFF)
|
|
|
|
mock_api._on_is_on_updated(True)
|
|
assert hass.states.is_state(REMOTE_ENTITY, STATE_ON)
|
|
|
|
mock_api._on_current_app_updated("activity1")
|
|
assert (
|
|
hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "activity1"
|
|
)
|
|
|
|
mock_api._on_current_app_updated("com.google.android.youtube.tv")
|
|
assert (
|
|
hass.states.get(REMOTE_ENTITY).attributes.get("current_activity") == "YouTube"
|
|
)
|
|
|
|
mock_api._on_is_available_updated(False)
|
|
assert hass.states.is_state(REMOTE_ENTITY, STATE_UNAVAILABLE)
|
|
|
|
mock_api._on_is_available_updated(True)
|
|
assert hass.states.is_state(REMOTE_ENTITY, STATE_ON)
|
|
|
|
|
|
async def test_remote_toggles(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test the Android TV Remote toggles."""
|
|
new_options = {"apps": {"com.google.android.youtube.tv": {"app_name": "YouTube"}}}
|
|
mock_config_entry.add_to_hass(hass)
|
|
hass.config_entries.async_update_entry(mock_config_entry, options=new_options)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"turn_off",
|
|
{"entity_id": REMOTE_ENTITY},
|
|
blocking=True,
|
|
)
|
|
mock_api._on_is_on_updated(False)
|
|
|
|
mock_api.send_key_command.assert_called_with("POWER", "SHORT")
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"turn_on",
|
|
{"entity_id": REMOTE_ENTITY},
|
|
blocking=True,
|
|
)
|
|
mock_api._on_is_on_updated(True)
|
|
|
|
mock_api.send_key_command.assert_called_with("POWER", "SHORT")
|
|
assert mock_api.send_key_command.call_count == 2
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"turn_on",
|
|
{"entity_id": REMOTE_ENTITY, "activity": "activity1"},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_api.send_key_command.send_launch_app_command("activity1")
|
|
assert mock_api.send_key_command.call_count == 2
|
|
assert mock_api.send_launch_app_command.call_count == 1
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"turn_on",
|
|
{"entity_id": REMOTE_ENTITY, "activity": "YouTube"},
|
|
blocking=True,
|
|
)
|
|
|
|
mock_api.send_key_command.send_launch_app_command("com.google.android.youtube.tv")
|
|
assert mock_api.send_key_command.call_count == 2
|
|
assert mock_api.send_launch_app_command.call_count == 2
|
|
|
|
|
|
async def test_remote_send_command(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test remote.send_command service."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "DPAD_LEFT",
|
|
"num_repeats": 2,
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
mock_api.send_key_command.assert_called_with("DPAD_LEFT", "SHORT")
|
|
assert mock_api.send_key_command.call_count == 2
|
|
|
|
|
|
async def test_remote_send_command_multiple(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test remote.send_command service with multiple commands."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": ["DPAD_LEFT", "DPAD_UP"],
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [
|
|
call("DPAD_LEFT", "SHORT"),
|
|
call("DPAD_UP", "SHORT"),
|
|
]
|
|
|
|
|
|
async def test_remote_send_command_with_hold_secs(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test remote.send_command service with hold_secs."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "DPAD_RIGHT",
|
|
"delay_secs": 0.01,
|
|
"hold_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [
|
|
call("DPAD_RIGHT", "START_LONG"),
|
|
call("DPAD_RIGHT", "END_LONG"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("command", "expected_call"),
|
|
[
|
|
("start_long:DPAD_DOWN", call("DPAD_DOWN", "START_LONG")),
|
|
("end_long:DPAD_DOWN", call("DPAD_DOWN", "END_LONG")),
|
|
("short:DPAD_DOWN", call("DPAD_DOWN", "SHORT")),
|
|
("START_LONG:DPAD_DOWN", call("DPAD_DOWN", "START_LONG")),
|
|
],
|
|
ids=["start", "end", "short", "uppercase_prefix"],
|
|
)
|
|
async def test_remote_send_command_with_direction_prefix(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_api: MagicMock,
|
|
command: str,
|
|
expected_call: object,
|
|
) -> None:
|
|
"""Test remote.send_command emits a single directional event for prefixed commands."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": command,
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [expected_call]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"command",
|
|
[
|
|
"text:hello world",
|
|
"voice:something",
|
|
"DPAD_DOWN:WITH_COLON",
|
|
":leading_colon",
|
|
],
|
|
ids=["text_prefix", "unknown_prefix", "embedded_colon", "leading_colon"],
|
|
)
|
|
async def test_remote_send_command_unknown_prefix_passes_through(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_api: MagicMock,
|
|
command: str,
|
|
) -> None:
|
|
"""Test that commands with non-direction colon prefixes are forwarded verbatim.
|
|
|
|
The integration only strips prefixes that match the allowlist;
|
|
other colon-using conventions (notably the lib's own ``text:`` prefix for
|
|
keyboard text) must reach the underlying library unchanged so it can apply
|
|
its own routing.
|
|
"""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": command,
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [call(command, "SHORT")]
|
|
|
|
|
|
async def test_remote_send_command_direction_prefix_pair(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test that a press-down/release-up pair produces exactly two events.
|
|
|
|
This is the live-press scenario: a UI sends START_LONG on pointerdown and
|
|
END_LONG on pointerup as separate service calls. Together they must produce
|
|
no extra SHORT or sleep-driven events.
|
|
"""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "start_long:DPAD_CENTER",
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "end_long:DPAD_CENTER",
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [
|
|
call("DPAD_CENTER", "START_LONG"),
|
|
call("DPAD_CENTER", "END_LONG"),
|
|
]
|
|
|
|
|
|
async def test_remote_send_command_direction_prefix_with_hold_secs_raises(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test that combining a direction prefix with hold_secs raises."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError,
|
|
match='Command "start_long:DPAD_RIGHT" combines a direction prefix with hold_secs',
|
|
):
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "start_long:DPAD_RIGHT",
|
|
"delay_secs": 0.01,
|
|
"hold_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == []
|
|
|
|
|
|
async def test_remote_send_command_empty_key_code_raises(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test that a direction prefix without a key code raises."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError,
|
|
match='Command "SHORT:" is missing a key code after the direction prefix',
|
|
):
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "SHORT:",
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == []
|
|
|
|
|
|
async def test_remote_connection_closed(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_api: MagicMock
|
|
) -> None:
|
|
"""Test commands raise HomeAssistantError if ConnectionClosed."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
mock_api.send_key_command.side_effect = ConnectionClosed()
|
|
with pytest.raises(
|
|
HomeAssistantError, match="Connection to the Android TV device is closed"
|
|
):
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"send_command",
|
|
{
|
|
"entity_id": REMOTE_ENTITY,
|
|
"command": "DPAD_LEFT",
|
|
"delay_secs": 0.01,
|
|
},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_key_command.mock_calls == [call("DPAD_LEFT", "SHORT")]
|
|
|
|
mock_api.send_launch_app_command.side_effect = ConnectionClosed()
|
|
with pytest.raises(
|
|
HomeAssistantError, match="Connection to the Android TV device is closed"
|
|
):
|
|
await hass.services.async_call(
|
|
"remote",
|
|
"turn_on",
|
|
{"entity_id": REMOTE_ENTITY, "activity": "activity1"},
|
|
blocking=True,
|
|
)
|
|
assert mock_api.send_launch_app_command.mock_calls == [call("activity1")]
|