diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 0be5957a29a..677487945be 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = ["media_player", "remote"] SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 4a712ba053e..8474849bdaa 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -2,7 +2,7 @@ "domain": "directv", "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": ["directv==0.2.0"], + "requirements": ["directv==0.3.0"], "dependencies": [], "codeowners": ["@ctalkington"], "quality_scale": "gold", diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py new file mode 100644 index 00000000000..8bc7c220833 --- /dev/null +++ b/homeassistant/components/directv/remote.py @@ -0,0 +1,106 @@ +"""Support for the DIRECTV remote.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Iterable, List + +from directv import DIRECTV, DIRECTVError + +from homeassistant.components.remote import RemoteDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import DIRECTVEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=2) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List, bool], None], +) -> bool: + """Load DirecTV remote based on a config entry.""" + dtv = hass.data[DOMAIN][entry.entry_id] + entities = [] + + for location in dtv.device.locations: + entities.append( + DIRECTVRemote( + dtv=dtv, name=str.title(location.name), address=location.address, + ) + ) + + async_add_entities(entities, True) + + +class DIRECTVRemote(DIRECTVEntity, RemoteDevice): + """Device that sends commands to a DirecTV receiver.""" + + def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: + """Initialize DirecTV remote.""" + super().__init__( + dtv=dtv, name=name, address=address, + ) + + self._available = False + self._is_on = True + + @property + def available(self): + """Return if able to retrieve information from device or not.""" + return self._available + + @property + def unique_id(self): + """Return a unique ID.""" + if self._address == "0": + return self.dtv.device.info.receiver_id + + return self._address + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._is_on + + async def async_update(self) -> None: + """Update device state.""" + status = await self.dtv.status(self._address) + + if status in ("active", "standby"): + self._available = True + self._is_on = status == "active" + else: + self._available = False + self._is_on = False + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self.dtv.remote("poweron", self._address) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self.dtv.remote("poweroff", self._address) + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a command to a device. + + Supported keys: power, poweron, poweroff, format, + pause, rew, replay, stop, advance, ffwd, record, + play, guide, active, list, exit, back, menu, info, + up, down, left, right, select, red, green, yellow, + blue, chanup, chandown, prev, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, dash, enter + """ + for single_command in command: + try: + await self.dtv.remote(single_command, self._address) + except DIRECTVError: + _LOGGER.exception( + "Sending command %s to device %s failed", + single_command, + self._device_id, + ) diff --git a/requirements_all.txt b/requirements_all.txt index d3d1f669df9..e5916a146fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -447,7 +447,7 @@ deluge-client==1.7.1 denonavr==0.8.1 # homeassistant.components.directv -directv==0.2.0 +directv==0.3.0 # homeassistant.components.discogs discogs_client==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24986a574a1..d1bfa69bbd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ defusedxml==0.6.0 denonavr==0.8.1 # homeassistant.components.directv -directv==0.2.0 +directv==0.3.0 # homeassistant.components.updater distro==1.4.0 diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py new file mode 100644 index 00000000000..1e598b35892 --- /dev/null +++ b/tests/components/directv/test_remote.py @@ -0,0 +1,130 @@ +"""The tests for the DirecTV remote platform.""" +from typing import Any, List + +from asynctest import patch + +from homeassistant.components.remote import ( + ATTR_COMMAND, + ATTR_DELAY_SECS, + ATTR_DEVICE, + ATTR_NUM_REPEATS, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.components.directv import setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +ATTR_UNIQUE_ID = "unique_id" +CLIENT_ENTITY_ID = f"{REMOTE_DOMAIN}.client" +MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.host" +UNAVAILABLE_ENTITY_ID = f"{REMOTE_DOMAIN}.unavailable_client" + +# pylint: disable=redefined-outer-name + + +async def async_send_command( + hass: HomeAssistantType, + command: List[str], + entity_id: Any = ENTITY_MATCH_ALL, + device: str = None, + num_repeats: str = None, + delay_secs: str = None, +) -> None: + """Send a command to a device.""" + data = {ATTR_COMMAND: command} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if device: + data[ATTR_DEVICE] = device + + if num_repeats: + data[ATTR_NUM_REPEATS] = num_repeats + + if delay_secs: + data[ATTR_DELAY_SECS] = delay_secs + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_SEND_COMMAND, data) + + +async def async_turn_on( + hass: HomeAssistantType, entity_id: Any = ENTITY_MATCH_ALL +) -> None: + """Turn on device.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_ON, data) + + +async def async_turn_off( + hass: HomeAssistantType, entity_id: Any = ENTITY_MATCH_ALL +) -> None: + """Turn off remote.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(REMOTE_DOMAIN, SERVICE_TURN_OFF, data) + + +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setup with basic config.""" + await setup_integration(hass, aioclient_mock) + assert hass.states.get(MAIN_ENTITY_ID) + assert hass.states.get(CLIENT_ENTITY_ID) + assert hass.states.get(UNAVAILABLE_ENTITY_ID) + + +async def test_unique_id( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test unique id.""" + await setup_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + main = entity_registry.async_get(MAIN_ENTITY_ID) + assert main.unique_id == "028877455858" + + client = entity_registry.async_get(CLIENT_ENTITY_ID) + assert client.unique_id == "2CA17D1CD30X" + + unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) + assert unavailable_client.unique_id == "9XXXXXXXXXX9" + + +async def test_main_services( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the different services.""" + await setup_integration(hass, aioclient_mock) + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_off(hass, MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("poweroff", "0") + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_on(hass, MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("poweron", "0") + + with patch("directv.DIRECTV.remote") as remote_mock: + await async_send_command(hass, ["dash"], MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("dash", "0")