Files

396 lines
14 KiB
Python

"""Tests for the Infrared integration setup."""
import re
from unittest.mock import AsyncMock, Mock
from freezegun.api import FrozenDateTimeFactory
from infrared_protocols.commands.nec import NECCommand
import pytest
from homeassistant.components.infrared import (
DATA_COMPONENT,
DOMAIN,
InfraredReceivedSignal,
async_get_emitters,
async_get_receivers,
async_send_command,
async_subscribe_receiver,
)
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .common import MockInfraredEmitterEntity, MockInfraredReceiverEntity
from tests.common import (
MockConfigEntry,
MockModule,
MockPlatform,
mock_config_flow,
mock_integration,
mock_platform,
mock_restore_cache,
)
TEST_DOMAIN = "test"
TEST_COMMAND = NECCommand(address=0x04FB, command=0x08F7, modulation=38000)
async def test_get_entities_component_not_loaded(hass: HomeAssistant) -> None:
"""Test getting entities when the component is not loaded."""
assert async_get_emitters(hass) == []
assert async_get_receivers(hass) == []
@pytest.mark.usefixtures("init_infrared")
async def test_get_entities_empty(hass: HomeAssistant) -> None:
"""Test getting entities when none are registered."""
assert async_get_emitters(hass) == []
assert async_get_receivers(hass) == []
async def test_get_entities_filters_by_type(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
) -> None:
"""Test get_emitters/get_receivers return only entities of the matching type."""
assert async_get_emitters(hass) == [mock_infrared_emitter_entity.entity_id]
assert async_get_receivers(hass) == [mock_infrared_receiver_entity.entity_id]
@pytest.mark.usefixtures(
"mock_infrared_emitter_entity", "mock_infrared_receiver_entity"
)
async def test_infrared_entities_initial_state(hass: HomeAssistant) -> None:
"""Test infrared entities have no state before any command is sent."""
assert (emitter_state := hass.states.get("infrared.test_ir_emitter")) is not None
assert emitter_state.state == STATE_UNKNOWN
assert (receiver_state := hass.states.get("infrared.test_ir_receiver")) is not None
assert receiver_state.state == STATE_UNKNOWN
async def test_async_send_command_success(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test sending command via async_send_command helper."""
now = dt_util.utcnow()
freezer.move_to(now)
await async_send_command(hass, mock_infrared_emitter_entity.entity_id, TEST_COMMAND)
assert len(mock_infrared_emitter_entity.send_command_calls) == 1
assert mock_infrared_emitter_entity.send_command_calls[0] is TEST_COMMAND
state = hass.states.get("infrared.test_ir_emitter")
assert state is not None
assert state.state == now.isoformat(timespec="milliseconds")
async def test_async_send_command_error_does_not_update_state(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
) -> None:
"""Test that state is not updated when async_send_command raises an error."""
state = hass.states.get("infrared.test_ir_emitter")
assert state is not None
assert state.state == STATE_UNKNOWN
mock_infrared_emitter_entity.async_send_command = AsyncMock(
side_effect=HomeAssistantError("Transmission failed")
)
with pytest.raises(HomeAssistantError, match="Transmission failed"):
await async_send_command(
hass, mock_infrared_emitter_entity.entity_id, TEST_COMMAND
)
state = hass.states.get("infrared.test_ir_emitter")
assert state is not None
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures("init_infrared")
async def test_async_send_command_entity_not_found(hass: HomeAssistant) -> None:
"""Test async_send_command raises error when entity not found."""
with pytest.raises(
HomeAssistantError,
match=re.escape("Infrared entity `infrared.nonexistent_entity` not found"),
):
await async_send_command(hass, "infrared.nonexistent_entity", TEST_COMMAND)
async def test_async_send_command_rejects_receiver(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
) -> None:
"""Test async_send_command rejects a receiver entity."""
with pytest.raises(
HomeAssistantError,
match=re.escape(
f"Infrared entity `{mock_infrared_receiver_entity.entity_id}` not found"
),
):
await async_send_command(
hass, mock_infrared_receiver_entity.entity_id, TEST_COMMAND
)
async def test_async_send_command_component_not_loaded(hass: HomeAssistant) -> None:
"""Test async_send_command raises error when component not loaded."""
with pytest.raises(HomeAssistantError, match="component_not_loaded"):
await async_send_command(hass, "infrared.some_entity", TEST_COMMAND)
@pytest.mark.parametrize(
("restored_value", "expected_state"),
[
("2026-01-01T12:00:00.000+00:00", "2026-01-01T12:00:00.000+00:00"),
(STATE_UNAVAILABLE, STATE_UNKNOWN),
],
)
async def test_infrared_entity_state_restore(
hass: HomeAssistant, restored_value: str, expected_state: str
) -> None:
"""Test infrared entity state restore."""
mock_restore_cache(
hass,
[
State("infrared.test_ir_emitter", restored_value),
State("infrared.test_ir_receiver", restored_value),
],
)
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
component = hass.data[DATA_COMPONENT]
await component.async_add_entities(
[
MockInfraredEmitterEntity("test_ir_emitter"),
MockInfraredReceiverEntity("test_ir_receiver"),
]
)
assert (emitter_state := hass.states.get("infrared.test_ir_emitter")) is not None
assert emitter_state.state == expected_state
assert (receiver_state := hass.states.get("infrared.test_ir_receiver")) is not None
assert receiver_state.state == expected_state
async def test_async_subscribe_receiver_success(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test subscribing to a receiver via async_subscribe_receiver helper."""
now = dt_util.utcnow()
freezer.move_to(now)
signal_callback = Mock()
unsubscribe = async_subscribe_receiver(
hass, mock_infrared_receiver_entity.entity_id, signal_callback
)
signal = InfraredReceivedSignal(timings=[100, 200, 300], modulation=38000)
mock_infrared_receiver_entity._handle_received_signal(signal)
assert signal_callback.call_count == 1
assert signal_callback.call_args[0][0] is signal
state = hass.states.get("infrared.test_ir_receiver")
assert state is not None
assert state.state == now.isoformat(timespec="milliseconds")
unsubscribe()
mock_infrared_receiver_entity._handle_received_signal(signal)
assert signal_callback.call_count == 1
async def test_handle_received_signal_isolates_callback_errors(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test a failing subscriber does not prevent other subscribers from running."""
failing_callback = Mock(side_effect=RuntimeError("boom"))
working_callback = Mock()
async_subscribe_receiver(
hass, mock_infrared_receiver_entity.entity_id, failing_callback
)
async_subscribe_receiver(
hass, mock_infrared_receiver_entity.entity_id, working_callback
)
signal = InfraredReceivedSignal(timings=[100, 200, 300])
mock_infrared_receiver_entity._handle_received_signal(signal)
failing_callback.assert_called_once_with(signal)
working_callback.assert_called_once_with(signal)
assert "Error in signal callback" in caplog.text
async def test_handle_received_signal_unsubscribe_during_dispatch(
hass: HomeAssistant,
mock_infrared_receiver_entity: MockInfraredReceiverEntity,
) -> None:
"""Test a subscriber can unsubscribe itself during dispatch without error."""
other_callback = Mock()
def unsubscribing_callback(signal: InfraredReceivedSignal) -> None:
unsubscribe()
self_unsub_mock = Mock(side_effect=unsubscribing_callback)
unsubscribe = async_subscribe_receiver(
hass, mock_infrared_receiver_entity.entity_id, self_unsub_mock
)
async_subscribe_receiver(
hass, mock_infrared_receiver_entity.entity_id, other_callback
)
signal = InfraredReceivedSignal(timings=[100, 200, 300])
mock_infrared_receiver_entity._handle_received_signal(signal)
self_unsub_mock.assert_called_once_with(signal)
other_callback.assert_called_once_with(signal)
mock_infrared_receiver_entity._handle_received_signal(signal)
self_unsub_mock.assert_called_once_with(signal)
assert other_callback.call_count == 2
@pytest.mark.usefixtures("init_infrared")
@pytest.mark.parametrize(
"entity_id_or_uuid",
["infrared.nonexistent_entity", "invalid-id"],
)
async def test_async_subscribe_receiver_not_found(
hass: HomeAssistant, entity_id_or_uuid: str
) -> None:
"""Test async_subscribe_receiver raises when the entity is missing or invalid."""
with pytest.raises(
HomeAssistantError,
match=re.escape(f"Infrared receiver entity `{entity_id_or_uuid}` not found"),
):
async_subscribe_receiver(hass, entity_id_or_uuid, lambda _: None)
async def test_async_subscribe_receiver_rejects_emitter(
hass: HomeAssistant,
mock_infrared_emitter_entity: MockInfraredEmitterEntity,
) -> None:
"""Test async_subscribe_receiver rejects an emitter entity."""
with pytest.raises(
HomeAssistantError,
match=re.escape(
f"Infrared receiver entity `{mock_infrared_emitter_entity.entity_id}`"
" not found"
),
):
async_subscribe_receiver(
hass, mock_infrared_emitter_entity.entity_id, lambda _: None
)
async def test_async_subscribe_receiver_component_not_loaded(
hass: HomeAssistant,
) -> None:
"""Test async_subscribe_receiver raises error when component not loaded."""
with pytest.raises(HomeAssistantError, match="component_not_loaded"):
async_subscribe_receiver(hass, "infrared.some_entity", lambda _: None)
@pytest.mark.usefixtures("init_infrared")
async def test_name(hass: HomeAssistant) -> None:
"""Test entity name / device class naming fallback."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setups(
config_entry, [Platform.INFRARED]
)
return True
class MockFlow(ConfigFlow):
"""Test flow."""
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
mock_integration(
hass,
MockModule(
TEST_DOMAIN,
async_setup_entry=async_setup_entry_init,
),
)
# Unnamed emitter without has_entity_name -> no name
emitter1 = MockInfraredEmitterEntity("test_emitter1", name=None)
emitter1.entity_id = "infrared.test_emitter1"
emitter1._attr_has_entity_name = False
# Unnamed emitter with has_entity_name True -> name set from device class
emitter2 = MockInfraredEmitterEntity("test_emitter2", name=None)
emitter2.entity_id = "infrared.test_emitter2"
emitter2._attr_has_entity_name = True
# Unnamed receiver without has_entity_name -> no name
receiver1 = MockInfraredReceiverEntity("test_receiver1", name=None)
receiver1.entity_id = "infrared.test_receiver1"
receiver1._attr_has_entity_name = False
# Unnamed receiver with has_entity_name True -> name set from device class
receiver2 = MockInfraredReceiverEntity("test_receiver2", name=None)
receiver2.entity_id = "infrared.test_receiver2"
receiver2._attr_has_entity_name = True
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up test infrared platform via config entry."""
async_add_entities([emitter1, emitter2, receiver1, receiver2])
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
config_entry.add_to_hass(hass)
with mock_config_flow(TEST_DOMAIN, MockFlow):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
state1 = hass.states.get("infrared.test_emitter1")
assert state1 is not None
assert state1.attributes == {"device_class": "emitter"}
state2 = hass.states.get("infrared.test_emitter2")
assert state2 is not None
assert state2.attributes == {
"device_class": "emitter",
"friendly_name": "Infrared emitter",
}
state3 = hass.states.get("infrared.test_receiver1")
assert state3 is not None
assert state3.attributes == {"device_class": "receiver"}
state4 = hass.states.get("infrared.test_receiver2")
assert state4 is not None
assert state4.attributes == {
"device_class": "receiver",
"friendly_name": "Infrared receiver",
}