2019-04-03 15:40:03 +00:00
|
|
|
"""Media Player component to integrate TVs exposing the Joint Space API."""
|
2021-02-11 20:37:53 +00:00
|
|
|
from typing import Any, Dict
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
from homeassistant import config_entries
|
2020-09-06 13:52:59 +00:00
|
|
|
from homeassistant.components.media_player import (
|
2021-02-11 20:37:53 +00:00
|
|
|
DEVICE_CLASS_TV,
|
2020-09-06 13:52:59 +00:00
|
|
|
PLATFORM_SCHEMA,
|
|
|
|
BrowseMedia,
|
|
|
|
MediaPlayerEntity,
|
|
|
|
)
|
2019-02-08 22:18:18 +00:00
|
|
|
from homeassistant.components.media_player.const import (
|
2020-09-08 14:42:01 +00:00
|
|
|
MEDIA_CLASS_CHANNEL,
|
2020-09-11 11:08:13 +00:00
|
|
|
MEDIA_CLASS_DIRECTORY,
|
2019-11-25 08:41:37 +00:00
|
|
|
MEDIA_TYPE_CHANNEL,
|
2020-09-06 13:52:59 +00:00
|
|
|
MEDIA_TYPE_CHANNELS,
|
2020-09-04 20:12:31 +00:00
|
|
|
SUPPORT_BROWSE_MEDIA,
|
2019-07-31 19:25:30 +00:00
|
|
|
SUPPORT_NEXT_TRACK,
|
2019-11-25 08:41:37 +00:00
|
|
|
SUPPORT_PLAY_MEDIA,
|
2019-07-31 19:25:30 +00:00
|
|
|
SUPPORT_PREVIOUS_TRACK,
|
|
|
|
SUPPORT_SELECT_SOURCE,
|
|
|
|
SUPPORT_TURN_OFF,
|
|
|
|
SUPPORT_TURN_ON,
|
|
|
|
SUPPORT_VOLUME_MUTE,
|
|
|
|
SUPPORT_VOLUME_SET,
|
|
|
|
SUPPORT_VOLUME_STEP,
|
|
|
|
)
|
2020-09-04 20:12:31 +00:00
|
|
|
from homeassistant.components.media_player.errors import BrowseError
|
2021-02-11 20:37:53 +00:00
|
|
|
from homeassistant.components.philips_js import PhilipsTVDataUpdateCoordinator
|
2016-11-03 02:19:53 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_API_VERSION,
|
|
|
|
CONF_HOST,
|
|
|
|
CONF_NAME,
|
|
|
|
STATE_OFF,
|
|
|
|
STATE_ON,
|
|
|
|
)
|
2021-02-11 20:37:53 +00:00
|
|
|
from homeassistant.core import callback
|
2018-09-09 12:26:06 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2021-02-11 20:37:53 +00:00
|
|
|
from homeassistant.helpers.typing import HomeAssistantType
|
|
|
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
from . import LOGGER as _LOGGER
|
|
|
|
from .const import CONF_SYSTEM, DOMAIN
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
SUPPORT_PHILIPS_JS = (
|
|
|
|
SUPPORT_TURN_OFF
|
|
|
|
| SUPPORT_VOLUME_STEP
|
|
|
|
| SUPPORT_VOLUME_SET
|
|
|
|
| SUPPORT_VOLUME_MUTE
|
|
|
|
| SUPPORT_SELECT_SOURCE
|
|
|
|
| SUPPORT_NEXT_TRACK
|
|
|
|
| SUPPORT_PREVIOUS_TRACK
|
|
|
|
| SUPPORT_PLAY_MEDIA
|
2020-09-04 20:12:31 +00:00
|
|
|
| SUPPORT_BROWSE_MEDIA
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-11-30 21:07:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ON_ACTION = "turn_on_action"
|
2018-02-07 17:35:08 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEFAULT_API_VERSION = "1"
|
2019-04-22 19:26:15 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PREFIX_SEPARATOR = ": "
|
|
|
|
PREFIX_SOURCE = "Input"
|
|
|
|
PREFIX_CHANNEL = "Channel"
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
PLATFORM_SCHEMA = vol.All(
|
|
|
|
cv.deprecated(CONF_HOST),
|
|
|
|
cv.deprecated(CONF_NAME),
|
|
|
|
cv.deprecated(CONF_API_VERSION),
|
|
|
|
cv.deprecated(CONF_ON_ACTION),
|
|
|
|
PLATFORM_SCHEMA.extend(
|
|
|
|
{
|
|
|
|
vol.Required(CONF_HOST): cv.string,
|
|
|
|
vol.Remove(CONF_NAME): cv.string,
|
|
|
|
vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string,
|
|
|
|
vol.Remove(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
|
|
|
}
|
|
|
|
),
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
|
2019-04-22 19:26:15 +00:00
|
|
|
def _inverted(data):
|
|
|
|
return {v: k for k, v in data.items()}
|
|
|
|
|
|
|
|
|
2018-08-24 14:37:30 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Set up the Philips TV platform."""
|
2021-02-11 20:37:53 +00:00
|
|
|
hass.async_create_task(
|
|
|
|
hass.config_entries.flow.async_init(
|
|
|
|
DOMAIN,
|
|
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
|
|
data=config,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistantType,
|
|
|
|
config_entry: config_entries.ConfigEntry,
|
|
|
|
async_add_entities,
|
|
|
|
):
|
|
|
|
"""Set up the configuration entry."""
|
|
|
|
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
|
|
|
async_add_entities(
|
|
|
|
[
|
|
|
|
PhilipsTVMediaPlayer(
|
|
|
|
coordinator,
|
|
|
|
config_entry.data[CONF_SYSTEM],
|
|
|
|
config_entry.unique_id or config_entry.entry_id,
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
2016-11-03 02:19:53 +00:00
|
|
|
"""Representation of a Philips TV exposing the JointSpace API."""
|
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
coordinator: PhilipsTVDataUpdateCoordinator,
|
|
|
|
system: Dict[str, Any],
|
|
|
|
unique_id: str,
|
|
|
|
):
|
2016-11-03 02:19:53 +00:00
|
|
|
"""Initialize the Philips TV."""
|
2021-02-11 20:37:53 +00:00
|
|
|
self._tv = coordinator.api
|
|
|
|
self._coordinator = coordinator
|
2019-04-22 19:26:15 +00:00
|
|
|
self._sources = {}
|
|
|
|
self._channels = {}
|
|
|
|
self._supports = SUPPORT_PHILIPS_JS
|
2021-02-11 20:37:53 +00:00
|
|
|
self._system = system
|
|
|
|
self._unique_id = unique_id
|
|
|
|
super().__init__(coordinator)
|
|
|
|
self._update_from_coordinator()
|
2019-04-22 19:26:15 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
def _update_soon(self):
|
2019-04-22 19:26:15 +00:00
|
|
|
"""Reschedule update task."""
|
2021-02-11 20:37:53 +00:00
|
|
|
self.hass.add_job(self.coordinator.async_request_refresh)
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the device name."""
|
2021-02-11 20:37:53 +00:00
|
|
|
return self._system["name"]
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
2017-02-08 04:42:45 +00:00
|
|
|
def supported_features(self):
|
|
|
|
"""Flag media player features that are supported."""
|
2021-02-11 20:37:53 +00:00
|
|
|
supports = self._supports
|
|
|
|
if self._coordinator.turn_on:
|
|
|
|
supports |= SUPPORT_TURN_ON
|
|
|
|
return supports
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
"""Get the device state. An exception means OFF state."""
|
2019-04-22 19:26:15 +00:00
|
|
|
if self._tv.on:
|
|
|
|
return STATE_ON
|
|
|
|
return STATE_OFF
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def source(self):
|
|
|
|
"""Return the current input source."""
|
2020-09-04 20:12:31 +00:00
|
|
|
return self._sources.get(self._tv.source_id)
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def source_list(self):
|
|
|
|
"""List of available input sources."""
|
2020-09-04 20:12:31 +00:00
|
|
|
return list(self._sources.values())
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
def select_source(self, source):
|
|
|
|
"""Set the input source."""
|
2019-04-22 19:26:15 +00:00
|
|
|
data = source.split(PREFIX_SEPARATOR, 1)
|
2020-09-04 20:12:31 +00:00
|
|
|
if data[0] == PREFIX_SOURCE: # Legacy way to set source
|
2019-04-22 19:26:15 +00:00
|
|
|
source_id = _inverted(self._sources).get(data[1])
|
|
|
|
if source_id:
|
|
|
|
self._tv.setSource(source_id)
|
2020-09-04 20:12:31 +00:00
|
|
|
elif data[0] == PREFIX_CHANNEL: # Legacy way to set channel
|
2019-04-22 19:26:15 +00:00
|
|
|
channel_id = _inverted(self._channels).get(data[1])
|
|
|
|
if channel_id:
|
|
|
|
self._tv.setChannel(channel_id)
|
2020-09-04 20:12:31 +00:00
|
|
|
else:
|
|
|
|
source_id = _inverted(self._sources).get(source)
|
|
|
|
if source_id:
|
|
|
|
self._tv.setSource(source_id)
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def volume_level(self):
|
|
|
|
"""Volume level of the media player (0..1)."""
|
2019-04-22 19:26:15 +00:00
|
|
|
return self._tv.volume
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_volume_muted(self):
|
|
|
|
"""Boolean if volume is currently muted."""
|
2019-04-22 19:26:15 +00:00
|
|
|
return self._tv.muted
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
async def async_turn_on(self):
|
2018-02-07 17:35:08 +00:00
|
|
|
"""Turn on the device."""
|
2021-02-11 20:37:53 +00:00
|
|
|
await self._coordinator.turn_on.async_run(self.hass, self._context)
|
2018-02-07 17:35:08 +00:00
|
|
|
|
2016-11-03 02:19:53 +00:00
|
|
|
def turn_off(self):
|
|
|
|
"""Turn off the device."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._tv.sendKey("Standby")
|
2019-04-22 19:26:15 +00:00
|
|
|
self._tv.on = False
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
def volume_up(self):
|
|
|
|
"""Send volume up command."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._tv.sendKey("VolumeUp")
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
def volume_down(self):
|
|
|
|
"""Send volume down command."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._tv.sendKey("VolumeDown")
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-03 02:19:53 +00:00
|
|
|
|
|
|
|
def mute_volume(self, mute):
|
|
|
|
"""Send mute command."""
|
2019-04-29 01:27:35 +00:00
|
|
|
self._tv.setVolume(None, mute)
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2018-05-28 14:41:51 +00:00
|
|
|
def set_volume_level(self, volume):
|
|
|
|
"""Set volume level, range 0..1."""
|
2019-04-22 19:26:15 +00:00
|
|
|
self._tv.setVolume(volume, self._tv.muted)
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2018-05-28 14:41:51 +00:00
|
|
|
|
2016-11-30 21:07:57 +00:00
|
|
|
def media_previous_track(self):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Send rewind command."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._tv.sendKey("Previous")
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2016-11-30 21:07:57 +00:00
|
|
|
|
|
|
|
def media_next_track(self):
|
2017-09-23 15:15:46 +00:00
|
|
|
"""Send fast forward command."""
|
2019-07-31 19:25:30 +00:00
|
|
|
self._tv.sendKey("Next")
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2019-04-22 19:26:15 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def media_channel(self):
|
|
|
|
"""Get current channel if it's a channel."""
|
|
|
|
if self.media_content_type == MEDIA_TYPE_CHANNEL:
|
|
|
|
return self._channels.get(self._tv.channel_id)
|
|
|
|
return None
|
2016-11-30 21:07:57 +00:00
|
|
|
|
2016-11-03 02:19:53 +00:00
|
|
|
@property
|
|
|
|
def media_title(self):
|
|
|
|
"""Title of current playing media."""
|
2019-04-22 19:26:15 +00:00
|
|
|
if self.media_content_type == MEDIA_TYPE_CHANNEL:
|
|
|
|
return self._channels.get(self._tv.channel_id)
|
|
|
|
return self._sources.get(self._tv.source_id)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_type(self):
|
|
|
|
"""Return content type of playing media."""
|
2019-07-31 19:25:30 +00:00
|
|
|
if self._tv.source_id == "tv" or self._tv.source_id == "11":
|
2019-04-22 19:26:15 +00:00
|
|
|
return MEDIA_TYPE_CHANNEL
|
2019-07-31 19:25:30 +00:00
|
|
|
if self._tv.source_id is None and self._tv.channels:
|
2019-04-22 19:26:15 +00:00
|
|
|
return MEDIA_TYPE_CHANNEL
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def media_content_id(self):
|
|
|
|
"""Content type of current playing media."""
|
|
|
|
if self.media_content_type == MEDIA_TYPE_CHANNEL:
|
|
|
|
return self._channels.get(self._tv.channel_id)
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the state attributes."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return {"channel_list": list(self._channels.values())}
|
2019-04-22 19:26:15 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
@property
|
|
|
|
def device_class(self):
|
|
|
|
"""Return the device class."""
|
|
|
|
return DEVICE_CLASS_TV
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return unique identifier if known."""
|
|
|
|
return self._unique_id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return a device description for device registry."""
|
|
|
|
return {
|
|
|
|
"name": self._system["name"],
|
|
|
|
"identifiers": {
|
|
|
|
(DOMAIN, self._unique_id),
|
|
|
|
},
|
|
|
|
"model": self._system.get("model"),
|
|
|
|
"manufacturer": "Philips",
|
|
|
|
"sw_version": self._system.get("softwareversion"),
|
|
|
|
}
|
|
|
|
|
2019-04-22 19:26:15 +00:00
|
|
|
def play_media(self, media_type, media_id, **kwargs):
|
|
|
|
"""Play a piece of media."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
|
2019-04-22 19:26:15 +00:00
|
|
|
|
|
|
|
if media_type == MEDIA_TYPE_CHANNEL:
|
|
|
|
channel_id = _inverted(self._channels).get(media_id)
|
|
|
|
if channel_id:
|
|
|
|
self._tv.setChannel(channel_id)
|
2021-02-11 20:37:53 +00:00
|
|
|
self._update_soon()
|
2019-04-22 19:26:15 +00:00
|
|
|
else:
|
|
|
|
_LOGGER.error("Unable to find channel <%s>", media_id)
|
|
|
|
else:
|
|
|
|
_LOGGER.error("Unsupported media type <%s>", media_type)
|
2016-11-03 02:19:53 +00:00
|
|
|
|
2020-09-04 20:12:31 +00:00
|
|
|
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
|
|
|
"""Implement the websocket media browsing helper."""
|
|
|
|
if media_content_id not in (None, ""):
|
|
|
|
raise BrowseError(
|
|
|
|
f"Media not found: {media_content_type} / {media_content_id}"
|
|
|
|
)
|
|
|
|
|
2020-09-06 13:52:59 +00:00
|
|
|
return BrowseMedia(
|
|
|
|
title="Channels",
|
2020-09-11 11:08:13 +00:00
|
|
|
media_class=MEDIA_CLASS_DIRECTORY,
|
2020-09-06 13:52:59 +00:00
|
|
|
media_content_id="",
|
|
|
|
media_content_type=MEDIA_TYPE_CHANNELS,
|
|
|
|
can_play=False,
|
|
|
|
can_expand=True,
|
|
|
|
children=[
|
|
|
|
BrowseMedia(
|
|
|
|
title=channel,
|
2020-09-08 14:42:01 +00:00
|
|
|
media_class=MEDIA_CLASS_CHANNEL,
|
2020-09-06 13:52:59 +00:00
|
|
|
media_content_id=channel,
|
|
|
|
media_content_type=MEDIA_TYPE_CHANNEL,
|
|
|
|
can_play=True,
|
|
|
|
can_expand=False,
|
|
|
|
)
|
2020-09-04 20:12:31 +00:00
|
|
|
for channel in self._channels.values()
|
|
|
|
],
|
2020-09-06 13:52:59 +00:00
|
|
|
)
|
2020-09-04 20:12:31 +00:00
|
|
|
|
2021-02-11 20:37:53 +00:00
|
|
|
def _update_from_coordinator(self):
|
2019-04-22 19:26:15 +00:00
|
|
|
self._sources = {
|
2020-12-17 15:44:24 +00:00
|
|
|
srcid: source.get("name") or f"Source {srcid}"
|
2019-04-22 19:26:15 +00:00
|
|
|
for srcid, source in (self._tv.sources or {}).items()
|
|
|
|
}
|
|
|
|
|
|
|
|
self._channels = {
|
2020-12-17 15:44:24 +00:00
|
|
|
chid: channel.get("name") or f"Channel {chid}"
|
|
|
|
for chid, channel in (self._tv.channels or {}).items()
|
2019-04-22 19:26:15 +00:00
|
|
|
}
|
2021-02-11 20:37:53 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _handle_coordinator_update(self) -> None:
|
|
|
|
"""Handle updated data from the coordinator."""
|
|
|
|
self._update_from_coordinator()
|
|
|
|
super()._handle_coordinator_update()
|