core/tests/components/hdmi_cec/test_init.py

517 lines
16 KiB
Python

"""Tests for the HDMI-CEC component."""
from collections.abc import Generator
from datetime import timedelta
from typing import Any
from unittest.mock import ANY, MagicMock, PropertyMock, call, patch
import pytest
import voluptuous as vol
from homeassistant.components.hdmi_cec import (
DOMAIN,
EVENT_HDMI_CEC_UNAVAILABLE,
SERVICE_POWER_ON,
SERVICE_SELECT_DEVICE,
SERVICE_SEND_COMMAND,
SERVICE_STANDBY,
SERVICE_UPDATE_DEVICES,
SERVICE_VOLUME,
WATCHDOG_INTERVAL,
CecCommand,
KeyPressCommand,
KeyReleaseCommand,
PhysicalAddress,
parse_mapping,
)
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from . import assert_key_press_release
from .conftest import HDMINetworkCreator
from tests.common import (
MockEntity,
MockEntityPlatform,
async_capture_events,
async_fire_time_changed,
)
@pytest.fixture(name="mock_tcp_adapter")
def mock_tcp_adapter_fixture() -> Generator[MagicMock]:
"""Mock TcpAdapter."""
with patch(
"homeassistant.components.hdmi_cec.TcpAdapter", autospec=True
) as mock_tcp_adapter:
yield mock_tcp_adapter
@pytest.mark.parametrize(
("mapping", "expected"),
[
({}, []),
(
{
"TV": "0.0.0.0",
"Pi Zero": "1.0.0.0",
"Fire TV Stick": "2.1.0.0",
"Chromecast": "2.2.0.0",
"Another Device": "2.3.0.0",
"BlueRay player": "3.0.0.0",
},
[
("TV", "0.0.0.0"),
("Pi Zero", "1.0.0.0"),
("Fire TV Stick", "2.1.0.0"),
("Chromecast", "2.2.0.0"),
("Another Device", "2.3.0.0"),
("BlueRay player", "3.0.0.0"),
],
),
(
{
1: "Pi Zero",
2: {
1: "Fire TV Stick",
2: "Chromecast",
3: "Another Device",
},
3: "BlueRay player",
},
[
("Pi Zero", [1, 0, 0, 0]),
("Fire TV Stick", [2, 1, 0, 0]),
("Chromecast", [2, 2, 0, 0]),
("Another Device", [2, 3, 0, 0]),
("BlueRay player", [3, 0, 0, 0]),
],
),
],
)
def test_parse_mapping_physical_address(
mapping: dict[str, Any], expected: list[tuple[str, list[int]]]
) -> None:
"""Test the device config mapping function."""
result = parse_mapping(mapping)
result = [
(r[0], str(r[1]) if isinstance(r[1], PhysicalAddress) else r[1]) for r in result
]
assert result == expected
# Test Setup
async def test_setup_cec_adapter(
hass: HomeAssistant, mock_cec_adapter: MagicMock, mock_hdmi_network: MagicMock
) -> None:
"""Test the general setup of this component."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
mock_cec_adapter.assert_called_once_with(name="HA", activate_source=False)
mock_hdmi_network.assert_called_once()
call_args = mock_hdmi_network.call_args
assert call_args == call(mock_cec_adapter.return_value, loop=ANY)
assert call_args.kwargs["loop"] in (None, hass.loop)
mock_hdmi_network_instance = mock_hdmi_network.return_value
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_hdmi_network_instance.start.assert_called_once_with()
mock_hdmi_network_instance.set_new_device_callback.assert_called_once()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
mock_hdmi_network_instance.stop.assert_called_once_with()
@pytest.mark.parametrize("osd_name", ["test", "test_a_long_name"])
async def test_setup_set_osd_name(
hass: HomeAssistant, osd_name: str, mock_cec_adapter: MagicMock
) -> None:
"""Test the setup of this component with the `osd_name` config setting."""
await async_setup_component(hass, DOMAIN, {DOMAIN: {"osd_name": osd_name}})
mock_cec_adapter.assert_called_once_with(name=osd_name[:12], activate_source=False)
async def test_setup_tcp_adapter(
hass: HomeAssistant, mock_tcp_adapter: MagicMock, mock_hdmi_network: MagicMock
) -> None:
"""Test the setup of this component with the TcpAdapter (`host` config setting)."""
host = "0.0.0.0"
await async_setup_component(hass, DOMAIN, {DOMAIN: {"host": host}})
mock_tcp_adapter.assert_called_once_with(host, name="HA", activate_source=False)
mock_hdmi_network.assert_called_once()
call_args = mock_hdmi_network.call_args
assert call_args == call(mock_tcp_adapter.return_value, loop=ANY)
assert call_args.kwargs["loop"] in (None, hass.loop)
mock_hdmi_network_instance = mock_hdmi_network.return_value
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_hdmi_network_instance.start.assert_called_once_with()
mock_hdmi_network_instance.set_new_device_callback.assert_called_once()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
mock_hdmi_network_instance.stop.assert_called_once_with()
# Test services
async def test_service_power_on(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the power on service call."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_POWER_ON,
{},
blocking=True,
)
mock_hdmi_network_instance.power_on.assert_called_once_with()
async def test_service_standby(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the standby service call."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_STANDBY,
{},
blocking=True,
)
mock_hdmi_network_instance.standby.assert_called_once_with()
async def test_service_select_device_alias(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the select device service call with a known alias."""
mock_hdmi_network_instance = await create_hdmi_network(
{"devices": {"Chromecast": "1.0.0.0"}}
)
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_DEVICE,
{"device": "Chromecast"},
blocking=True,
)
mock_hdmi_network_instance.active_source.assert_called_once()
physical_address = mock_hdmi_network_instance.active_source.call_args.args[0]
assert isinstance(physical_address, PhysicalAddress)
assert str(physical_address) == "1.0.0.0"
class MockCecEntity(MockEntity):
"""Mock CEC entity."""
@property
def extra_state_attributes(self):
"""Set the physical address in the attributes."""
return {"physical_address": self._values["physical_address"]}
async def test_service_select_device_entity(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the select device service call with an existing entity."""
platform = MockEntityPlatform(hass)
await platform.async_add_entities(
[MockCecEntity(name="hdmi_3", physical_address="3.0.0.0")]
)
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_DEVICE,
{"device": "test_domain.hdmi_3"},
blocking=True,
)
mock_hdmi_network_instance.active_source.assert_called_once()
physical_address = mock_hdmi_network_instance.active_source.call_args.args[0]
assert isinstance(physical_address, PhysicalAddress)
assert str(physical_address) == "3.0.0.0"
async def test_service_select_device_physical_address(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the select device service call with a raw physical address."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_SELECT_DEVICE,
{"device": "1.1.0.0"},
blocking=True,
)
mock_hdmi_network_instance.active_source.assert_called_once()
physical_address = mock_hdmi_network_instance.active_source.call_args.args[0]
assert isinstance(physical_address, PhysicalAddress)
assert str(physical_address) == "1.1.0.0"
async def test_service_update_devices(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator
) -> None:
"""Test the update devices service call."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_UPDATE_DEVICES,
{},
blocking=True,
)
mock_hdmi_network_instance.scan.assert_called_once_with()
@pytest.mark.parametrize(
("count", "call_count"),
[
(3, 3),
(1, 1),
(0, 0),
pytest.param(
"",
1,
marks=pytest.mark.xfail(
reason="While the code allows for an empty string the schema doesn't allow it",
raises=vol.MultipleInvalid,
),
),
],
)
@pytest.mark.parametrize(("direction", "key"), [("up", 65), ("down", 66)])
async def test_service_volume_x_times(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
count: int,
call_count: int,
direction: str,
key: int,
) -> None:
"""Test the volume service call with steps."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_VOLUME,
{direction: count},
blocking=True,
)
assert mock_hdmi_network_instance.send_command.call_count == call_count * 2
for i in range(call_count):
assert_key_press_release(
mock_hdmi_network_instance.send_command, i, dst=5, key=key
)
@pytest.mark.parametrize(("direction", "key"), [("up", 65), ("down", 66)])
async def test_service_volume_press(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
direction: str,
key: int,
) -> None:
"""Test the volume service call with press attribute."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_VOLUME,
{direction: "press"},
blocking=True,
)
mock_hdmi_network_instance.send_command.assert_called_once()
arg = mock_hdmi_network_instance.send_command.call_args.args[0]
assert isinstance(arg, KeyPressCommand)
assert arg.key == key
assert arg.dst == 5
@pytest.mark.parametrize(("direction", "key"), [("up", 65), ("down", 66)])
async def test_service_volume_release(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
direction: str,
key: int,
) -> None:
"""Test the volume service call with release attribute."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_VOLUME,
{direction: "release"},
blocking=True,
)
mock_hdmi_network_instance.send_command.assert_called_once()
arg = mock_hdmi_network_instance.send_command.call_args.args[0]
assert isinstance(arg, KeyReleaseCommand)
assert arg.dst == 5
@pytest.mark.parametrize(
("attr", "key"),
[
("toggle", 67),
("on", 101),
("off", 102),
pytest.param(
"",
101,
marks=pytest.mark.xfail(
reason="The documentation mention it's allowed to pass an empty string, but the schema does not allow this",
raises=vol.MultipleInvalid,
),
),
],
)
async def test_service_volume_mute(
hass: HomeAssistant, create_hdmi_network: HDMINetworkCreator, attr: str, key: int
) -> None:
"""Test the volume service call with mute."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_VOLUME,
{"mute": attr},
blocking=True,
)
assert mock_hdmi_network_instance.send_command.call_count == 2
assert_key_press_release(mock_hdmi_network_instance.send_command, key=key, dst=5)
@pytest.mark.parametrize(
("data", "expected"),
[
({"raw": "20:0D"}, "20:0d"),
pytest.param(
{"cmd": "36"},
"ff:36",
marks=pytest.mark.xfail(
reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect."
),
),
({"cmd": 54}, "ff:36"),
pytest.param(
{"cmd": "36", "src": "1", "dst": "0"},
"10:36",
marks=pytest.mark.xfail(
reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect."
),
),
({"cmd": 54, "src": "1", "dst": "0"}, "10:36"),
pytest.param(
{"cmd": "64", "src": "1", "dst": "0", "att": "4f:44"},
"10:64:4f:44",
marks=pytest.mark.xfail(
reason="`att` only accepts a int or a HEX value, it seems good to allow for raw data here.",
raises=vol.MultipleInvalid,
),
),
pytest.param(
{"cmd": "0A", "src": "1", "dst": "0", "att": "1B"},
"10:0a:1b",
marks=pytest.mark.xfail(
reason="The code tries to run `reduce` on this string and fails.",
raises=TypeError,
),
),
pytest.param(
{"cmd": "0A", "src": "1", "dst": "0", "att": "01"},
"10:0a:1b",
marks=pytest.mark.xfail(
reason="The code tries to run `reduce` on this as an `int` and fails.",
raises=TypeError,
),
),
pytest.param(
{"cmd": "0A", "src": "1", "dst": "0", "att": ["1B", "44"]},
"10:0a:1b:44",
marks=pytest.mark.xfail(
reason="While the code shows that it's possible to passthrough a list, the call schema does not allow it.",
raises=(vol.MultipleInvalid, TypeError),
),
),
],
)
async def test_service_send_command(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
data: dict[str, Any],
expected: str,
) -> None:
"""Test the send command service call."""
mock_hdmi_network_instance = await create_hdmi_network()
await hass.services.async_call(
DOMAIN,
SERVICE_SEND_COMMAND,
data,
blocking=True,
)
mock_hdmi_network_instance.send_command.assert_called_once()
command = mock_hdmi_network_instance.send_command.call_args.args[0]
assert isinstance(command, CecCommand)
assert str(command) == expected
@pytest.mark.parametrize(
("adapter_initialized_value", "watchdog_actions"), [(False, 1), (True, 0)]
)
async def test_watchdog(
hass: HomeAssistant,
create_hdmi_network: HDMINetworkCreator,
mock_cec_adapter: MagicMock,
adapter_initialized_value: bool,
watchdog_actions: int,
) -> None:
"""Test the watchdog when adapter is down/up."""
adapter_initialized = PropertyMock(return_value=adapter_initialized_value)
events = async_capture_events(hass, EVENT_HDMI_CEC_UNAVAILABLE)
mock_cec_adapter_instance = mock_cec_adapter.return_value
type(mock_cec_adapter_instance).initialized = adapter_initialized
mock_hdmi_network_instance = await create_hdmi_network()
mock_hdmi_network_instance.set_initialized_callback.assert_called_once()
callback = mock_hdmi_network_instance.set_initialized_callback.call_args.args[0]
callback()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=WATCHDOG_INTERVAL))
await hass.async_block_till_done()
adapter_initialized.assert_called_once_with()
assert len(events) == watchdog_actions
assert mock_cec_adapter_instance.init.call_count == watchdog_actions