diff --git a/CODEOWNERS b/CODEOWNERS index 23005cb5273..04918e979ee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -365,6 +365,7 @@ homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core +homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index b34dba3ad94..e03fea68fd7 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from websockets.exceptions import ConnectionClosed from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CUSTOMIZE, CONF_HOST, CONF_ICON, @@ -14,6 +15,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send DOMAIN = "webostv" @@ -23,6 +25,12 @@ CONF_STANDBY_CONNECTION = "standby_connection" DEFAULT_NAME = "LG webOS Smart TV" WEBOSTV_CONFIG_FILE = "webostv.conf" +SERVICE_BUTTON = "button" +ATTR_BUTTON = "button" + +SERVICE_COMMAND = "command" +ATTR_COMMAND = "command" + CUSTOMIZE_SCHEMA = vol.Schema( {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} ) @@ -50,6 +58,17 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) + +COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) + +SERVICE_TO_METHOD = { + SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, + SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, +} + _LOGGER = logging.getLogger(__name__) @@ -57,6 +76,18 @@ async def async_setup(hass, config): """Set up the LG WebOS TV platform.""" hass.data[DOMAIN] = {} + async def async_service_handler(service): + method = SERVICE_TO_METHOD.get(service.service) + data = service.data.copy() + data["method"] = method["method"] + async_dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema + ) + tasks = [async_setup_tv(hass, config, conf) for conf in config[DOMAIN]] if tasks: await asyncio.gather(*tasks) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 016f14f0f94..82d4942f83c 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -6,5 +6,5 @@ "aiopylgtv==0.2.4" ], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index fd47cf0a114..5e58cdf7a2f 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -24,13 +24,16 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CUSTOMIZE, CONF_HOST, CONF_NAME, + ENTITY_MATCH_ALL, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN @@ -131,7 +134,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): self._last_icon = None async def async_added_to_hass(self): - """Connect and subscribe to state updates.""" + """Connect and subscribe to dispatcher signals and state updates.""" + async_dispatcher_connect(self.hass, DOMAIN, self.async_signal_handler) + await self._client.register_state_update_callback( self.async_handle_state_update ) @@ -144,6 +149,17 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): """Call disconnect on removal.""" self._client.unregister_state_update_callback(self.async_handle_state_update) + async def async_signal_handler(self, data): + """Handle domain-specific signal by calling appropriate method.""" + entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: + params = { + key: value + for key, value in data.items() + if key not in ["entity_id", "method"] + } + await getattr(self, data["method"])(**params) + async def async_handle_state_update(self): """Update state from WebOsClient.""" self._current_source_id = self._client.current_appId @@ -406,3 +422,13 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): await self._client.channel_down() else: await self._client.rewind() + + @cmd + async def async_button(self, button): + """Send a button press.""" + await self._client.button(button) + + @cmd + async def async_command(self, command): + """Send a command.""" + await self._client.request(command) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml new file mode 100644 index 00000000000..137a6026eda --- /dev/null +++ b/homeassistant/components/webostv/services.yaml @@ -0,0 +1,26 @@ +# Describes the format for available webostv services + +button: + description: 'Send a button press command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + button: + description: Name of the button to press. Known possible values are + LEFT, RIGHT, DOWN, UP, HOME, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, + MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + example: 'LEFT' + +command: + description: 'Send a command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + command: + description: Endpoint of the command. Known valid endpoints are listed in + https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py + example: 'media.controls/rewind' + diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 023e0e2dc07..4dcda9eb908 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -9,7 +9,13 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_MUTED, SERVICE_SELECT_SOURCE, ) -from homeassistant.components.webostv import DOMAIN +from homeassistant.components.webostv import ( + ATTR_BUTTON, + ATTR_COMMAND, + DOMAIN, + SERVICE_BUTTON, + SERVICE_COMMAND, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -75,3 +81,34 @@ async def test_select_source_with_empty_source_list(hass, client): assert hass.states.is_state(ENTITY_ID, "playing") client.launch_app.assert_not_called() client.set_input.assert_not_called() + + +async def test_button(hass, client): + """Test generic button functionality.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_BUTTON: "test", + } + await hass.services.async_call(DOMAIN, SERVICE_BUTTON, data) + await hass.async_block_till_done() + + client.button.assert_called_once() + client.button.assert_called_with("test") + + +async def test_command(hass, client): + """Test generic button functionality.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_COMMAND: "test", + } + await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) + await hass.async_block_till_done() + + client.request.assert_called_with("test")