diff --git a/homeassistant/components/lg_netcast/__init__.py b/homeassistant/components/lg_netcast/__init__.py index c2509889760..d97464d9a9c 100644 --- a/homeassistant/components/lg_netcast/__init__.py +++ b/homeassistant/components/lg_netcast/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import config_validation as cv from .const import DOMAIN -PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER] +PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) diff --git a/homeassistant/components/lg_netcast/remote.py b/homeassistant/components/lg_netcast/remote.py new file mode 100644 index 00000000000..db5562a598e --- /dev/null +++ b/homeassistant/components/lg_netcast/remote.py @@ -0,0 +1,83 @@ +"""Remote control support for LG Netcast TV.""" + +from __future__ import annotations + +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any + +from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError +from requests import RequestException + +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 . import LgNetCastConfigEntry +from .const import ATTR_MANUFACTURER, DOMAIN + +VALID_COMMANDS: frozenset[str] = frozenset( + k + for k in vars(LG_COMMAND) + if not k.startswith("_") and isinstance(getattr(LG_COMMAND, k), int) +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: LgNetCastConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up LG Netcast Remote from a config entry.""" + client = config_entry.runtime_data + unique_id = config_entry.unique_id + if TYPE_CHECKING: + assert unique_id is not None + + async_add_entities([LgNetCastRemote(client, unique_id)]) + + +class LgNetCastRemote(RemoteEntity): + """Device that sends commands to an LG Netcast TV.""" + + _attr_has_entity_name = True + _attr_name = None + + def __init__(self, client: LgNetCastClient, unique_id: str) -> None: + """Initialize the LG Netcast remote.""" + self._client = client + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + ) + + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to the TV.""" + num_repeats = kwargs[ATTR_NUM_REPEATS] + + commands: list[int] = [] + for cmd in command: + if cmd not in VALID_COMMANDS: + raise ServiceValidationError(f"Unknown command: {cmd!r}") + commands.append(getattr(LG_COMMAND, cmd)) + for _ in range(num_repeats): + try: + with self._client as client: + for lg_command in commands: + client.send_command(lg_command) + except LgNetCastError, RequestException: + self._attr_is_on = False + self.schedule_update_ha_state() + return + + def turn_on(self, **kwargs: Any) -> None: + """Turn on is handled via a separate turn_on trigger.""" + raise NotImplementedError( + "Turning on the TV is not supported by the LG Netcast remote entity" + ) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off the TV.""" + self.send_command(["POWER"], **{ATTR_NUM_REPEATS: 1}) diff --git a/tests/components/lg_netcast/test_remote.py b/tests/components/lg_netcast/test_remote.py new file mode 100644 index 00000000000..facdbe80b08 --- /dev/null +++ b/tests/components/lg_netcast/test_remote.py @@ -0,0 +1,59 @@ +"""Tests for LG Netcast remote platform.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from pylgnetcast import LG_COMMAND +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from . import MODEL_NAME, setup_lgnetcast + +REMOTE_ENTITY_ID = f"{REMOTE_DOMAIN}.{MODEL_NAME.lower()}" + + +@pytest.fixture(autouse=True) +def mock_lg_netcast() -> Generator[MagicMock]: + """Mock LG Netcast library.""" + with patch( + "homeassistant.components.lg_netcast.LgNetCastClient" + ) as mock_client_class: + yield mock_client_class + + +async def test_send_command(hass: HomeAssistant, mock_lg_netcast: MagicMock) -> None: + """Test remote.send_command calls the client with the correct command code.""" + await setup_lgnetcast(hass) + context_client = mock_lg_netcast.return_value.__enter__.return_value + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: REMOTE_ENTITY_ID, ATTR_COMMAND: ["POWER"]}, + blocking=True, + ) + + context_client.send_command.assert_called_once_with(LG_COMMAND.POWER) + + +async def test_send_command_invalid( + hass: HomeAssistant, mock_lg_netcast: MagicMock +) -> None: + """Test remote.send_command raises ServiceValidationError for an unknown command name.""" + await setup_lgnetcast(hass) + + with pytest.raises(ServiceValidationError, match="Unknown command"): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: REMOTE_ENTITY_ID, ATTR_COMMAND: ["NOT_A_REAL_COMMAND"]}, + blocking=True, + )