mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Add remote platform to Lyngdorf (#179742)
This commit is contained in:
@@ -8,6 +8,7 @@ DEFAULT_DEVICE_NAME = "Lyngdorf"
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.MEDIA_PLAYER,
|
||||
Platform.NUMBER,
|
||||
Platform.REMOTE,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Remote platform for Lyngdorf integration."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from lyngdorf.device import Receiver
|
||||
from lyngdorf.exceptions import LyngdorfUnsupportedError
|
||||
|
||||
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import LyngdorfEntity
|
||||
from .models import LyngdorfConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: LyngdorfConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Lyngdorf remote from a config entry."""
|
||||
runtime_data = config_entry.runtime_data
|
||||
receiver = runtime_data.receiver
|
||||
|
||||
# The TDAI family has no remote keys at all, so it gets no remote entity.
|
||||
if not receiver.has_remote_keys:
|
||||
return
|
||||
|
||||
async_add_entities(
|
||||
[LyngdorfRemote(receiver, config_entry, runtime_data.device_info)]
|
||||
)
|
||||
|
||||
|
||||
class LyngdorfRemote(LyngdorfEntity, RemoteEntity):
|
||||
"""Lyngdorf remote entity."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: Receiver,
|
||||
config_entry: LyngdorfConfigEntry,
|
||||
device_info: DeviceInfo,
|
||||
) -> None:
|
||||
"""Initialize the remote."""
|
||||
super().__init__(receiver, device_info)
|
||||
if TYPE_CHECKING:
|
||||
assert config_entry.unique_id
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return whether the device is on."""
|
||||
return self._receiver.power_on
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
self._receiver.power_on = True
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
self._receiver.power_on = False
|
||||
|
||||
@override
|
||||
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
||||
"""Send a sequence of remote keys to the device."""
|
||||
# delay_secs is dropped: the library already paces its own writes.
|
||||
try:
|
||||
self._receiver.send_remote_commands(
|
||||
command, num_repeats=kwargs[ATTR_NUM_REPEATS]
|
||||
)
|
||||
except LyngdorfUnsupportedError as err:
|
||||
# The member value is what a caller sends: DIGIT_0 is "0", not "digit_0".
|
||||
keys = sorted(key.value for key in self._receiver.available_remote_keys)
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unsupported_remote_key",
|
||||
translation_placeholders={"keys": ", ".join(keys)},
|
||||
) from err
|
||||
@@ -106,6 +106,9 @@
|
||||
},
|
||||
"setup_timeout": {
|
||||
"message": "Timeout connecting to {host}"
|
||||
},
|
||||
"unsupported_remote_key": {
|
||||
"message": "This device does not have that remote key. It supports: {keys}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
from lyngdorf.const import LyngdorfModel
|
||||
from lyngdorf.device import Receiver
|
||||
from lyngdorf.models.base import NumericRange
|
||||
from lyngdorf.remote import RemoteKey
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lyngdorf.const import (
|
||||
@@ -63,7 +64,16 @@ def mock_receiver() -> Generator[MagicMock]:
|
||||
receiver = MagicMock(spec=Receiver)
|
||||
receiver.name = "Mock Lyngdorf"
|
||||
receiver.connected = True
|
||||
receiver.model = LyngdorfModel.MP_60
|
||||
receiver.has_remote_keys = True
|
||||
receiver.available_remote_keys = frozenset(
|
||||
{
|
||||
RemoteKey.UP,
|
||||
RemoteKey.DOWN,
|
||||
RemoteKey.ENTER,
|
||||
RemoteKey.MENU,
|
||||
RemoteKey.DIGIT_0,
|
||||
}
|
||||
)
|
||||
|
||||
# Diagnostics reports the whole receiver, so every property it reads
|
||||
# needs a value here; an unset one is a mock the response cannot encode.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[remote.mock_lyngdorf-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'remote',
|
||||
'entity_category': None,
|
||||
'entity_id': 'remote.mock_lyngdorf',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'lyngdorf',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '0050c27c76b2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[remote.mock_lyngdorf-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Mock Lyngdorf',
|
||||
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <RemoteEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'remote.mock_lyngdorf',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tests for the Lyngdorf remote platform."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lyngdorf.const import LyngdorfModel
|
||||
from lyngdorf.exceptions import LyngdorfUnsupportedError
|
||||
from lyngdorf.remote import RemoteKey, resolve_remote_key
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.remote import (
|
||||
ATTR_COMMAND,
|
||||
ATTR_NUM_REPEATS,
|
||||
DOMAIN as REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
REMOTE = "remote.mock_lyngdorf"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Only load the remote platform."""
|
||||
return [Platform.REMOTE]
|
||||
|
||||
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the remote entity."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_send_command(
|
||||
hass: HomeAssistant,
|
||||
mock_receiver: MagicMock,
|
||||
) -> None:
|
||||
"""Test sending a sequence of remote keys."""
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
{
|
||||
ATTR_ENTITY_ID: REMOTE,
|
||||
ATTR_COMMAND: [RemoteKey.MENU, RemoteKey.DOWN, RemoteKey.ENTER],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_receiver.send_remote_commands.assert_called_once_with(
|
||||
[RemoteKey.MENU, RemoteKey.DOWN, RemoteKey.ENTER], num_repeats=1
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_send_command_repeats(
|
||||
hass: HomeAssistant,
|
||||
mock_receiver: MagicMock,
|
||||
) -> None:
|
||||
"""Test the repeat count is passed through to the library."""
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
{
|
||||
ATTR_ENTITY_ID: REMOTE,
|
||||
ATTR_COMMAND: [RemoteKey.DOWN],
|
||||
ATTR_NUM_REPEATS: 3,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_receiver.send_remote_commands.assert_called_once_with(
|
||||
[RemoteKey.DOWN], num_repeats=3
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_send_unsupported_command(
|
||||
hass: HomeAssistant,
|
||||
mock_receiver: MagicMock,
|
||||
) -> None:
|
||||
"""Test a key the model does not have is reported to the user."""
|
||||
mock_receiver.send_remote_commands.side_effect = LyngdorfUnsupportedError(
|
||||
"no such key"
|
||||
)
|
||||
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
SERVICE_SEND_COMMAND,
|
||||
{ATTR_ENTITY_ID: REMOTE, ATTR_COMMAND: ["nonsense"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert err.value.translation_key == "unsupported_remote_key"
|
||||
# Every key named in the message must be one the library will accept.
|
||||
listed = err.value.translation_placeholders["keys"].split(", ")
|
||||
assert listed
|
||||
assert all(resolve_remote_key(key) is not None for key in listed)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "expected"),
|
||||
[
|
||||
(SERVICE_TURN_ON, True),
|
||||
(SERVICE_TURN_OFF, False),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_power(
|
||||
hass: HomeAssistant,
|
||||
mock_receiver: MagicMock,
|
||||
service: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""Test turning the device on and off from the remote."""
|
||||
await hass.services.async_call(
|
||||
REMOTE_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: REMOTE},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_receiver.power_on is expected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_receiver")
|
||||
async def test_no_entity_for_model_without_remote_keys(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_receiver: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test no remote entity is created for a model with no remote keys."""
|
||||
mock_receiver.has_remote_keys = False
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.lyngdorf.lookup_receiver_model",
|
||||
return_value=LyngdorfModel.TDAI_3400,
|
||||
),
|
||||
patch("homeassistant.components.lyngdorf.PLATFORMS", [Platform.REMOTE]),
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(REMOTE) is None
|
||||
assert entity_registry.async_get(REMOTE) is None
|
||||
Reference in New Issue
Block a user