diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 0b3cc1025cc..b75abc32e4b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -28,6 +28,7 @@ SERVICE_HUE = 'philips_hue' SERVICE_CAST = 'google_cast' SERVICE_NETGEAR = 'netgear_router' SERVICE_SONOS = 'sonos' +SERVICE_PLEX = 'plex_mediaserver' SERVICE_HANDLERS = { SERVICE_WEMO: "switch", @@ -35,6 +36,7 @@ SERVICE_HANDLERS = { SERVICE_HUE: "light", SERVICE_NETGEAR: 'device_tracker', SERVICE_SONOS: 'media_player', + SERVICE_PLEX: 'media_player', } @@ -88,6 +90,7 @@ def setup(hass, config): ATTR_DISCOVERED: info }) + # pylint: disable=unused-argument def start_discovery(event): """ Start discovering. """ netdisco = DiscoveryService(SCAN_INTERVAL) diff --git a/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png b/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png new file mode 100644 index 00000000000..97a1b4b352c Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png differ diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 294fccbb1f5..8040ef9c067 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -28,6 +28,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { discovery.SERVICE_CAST: 'cast', discovery.SERVICE_SONOS: 'sonos', + discovery.SERVICE_PLEX: 'plex', } SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 5fac9ecb0f0..b8267d286d3 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -6,38 +6,114 @@ Provides an interface to the Plex API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.plex.html """ +import os +import json import logging from datetime import timedelta +from urllib.parse import urlparse +from homeassistant.loader import get_component +import homeassistant.util as util from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) from homeassistant.const import ( - STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) -import homeassistant.util as util + DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_PLAYING, + STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) -REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/' - 'df2d0847e801d6d5cda920326d693cf75f304f1a.zip' - '#python-plexapi==1.0.2'] +REQUIREMENTS = ['plexapi==1.1.0'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +PLEX_CONFIG_FILE = 'plex.conf' + +# Map ip to request id for configuring +_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK -# pylint: disable=abstract-method, unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the plex platform. """ - from plexapi.myplex import MyPlexUser - from plexapi.exceptions import BadRequest +def config_from_file(filename, config=None): + ''' Small configuration file management function''' + if config: + # We're writing configuration + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +# pylint: disable=abstract-method, unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Sets up the plex platform. """ + + config = config_from_file(hass.config.path(PLEX_CONFIG_FILE)) + if len(config): + # Setup a configured PlexServer + host, token = config.popitem() + token = token['token'] + # Via discovery + elif discovery_info is not None: + # Parse discovery data + host = urlparse(discovery_info[1]).netloc + _LOGGER.info('Discovered PLEX server: %s', host) + + if host in _CONFIGURING: + return + token = None + else: + return + + setup_plexserver(host, token, hass, add_devices_callback) + + +# pylint: disable=too-many-branches +def setup_plexserver(host, token, hass, add_devices_callback): + ''' Setup a plexserver based on host parameter''' + import plexapi.server + import plexapi.exceptions + + try: + plexserver = plexapi.server.PlexServer('http://%s' % host, token) + except (plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound) as error: + _LOGGER.info(error) + # No token or wrong token + request_configuration(host, hass, add_devices_callback) + return + + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Discovery configuration done!') + + # Save config + if not config_from_file( + hass.config.path(PLEX_CONFIG_FILE), + {host: {'token': token}}): + _LOGGER.error('failed to save config file') + + _LOGGER.info('Connected to: htts://%s', host) - name = config.get('name', '') - user = config.get('user', '') - password = config.get('password', '') - plexuser = MyPlexUser.signin(user, password) - plexserver = plexuser.getResource(name).connect() plex_clients = {} plex_sessions = {} @@ -45,34 +121,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def update_devices(): """ Updates the devices objects. """ try: - devices = plexuser.devices() - except BadRequest: + devices = plexserver.clients() + except plexapi.exceptions.BadRequest: _LOGGER.exception("Error listing plex devices") return new_plex_clients = [] for device in devices: - if (all(x not in ['client', 'player'] for x in device.provides) - or 'PlexAPI' == device.product): + # For now, let's allow all deviceClass types + if device.deviceClass in ['badClient']: continue - if device.clientIdentifier not in plex_clients: + if device.machineIdentifier not in plex_clients: new_client = PlexClient(device, plex_sessions, update_devices, update_sessions) - plex_clients[device.clientIdentifier] = new_client + plex_clients[device.machineIdentifier] = new_client new_plex_clients.append(new_client) else: - plex_clients[device.clientIdentifier].set_device(device) + plex_clients[device.machineIdentifier].set_device(device) if new_plex_clients: - add_devices(new_plex_clients) + add_devices_callback(new_plex_clients) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_sessions(): """ Updates the sessions objects. """ try: sessions = plexserver.sessions() - except BadRequest: + except plexapi.exceptions.BadRequest: _LOGGER.exception("Error listing plex sessions") return @@ -84,10 +160,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None): update_sessions() +def request_configuration(host, hass, add_devices_callback): + """ Request configuration steps from the user. """ + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[host], "Failed to register, please try again.") + + return + + def plex_configuration_callback(data): + """ Actions to do when our configuration callback is called. """ + setup_plexserver(host, data.get('token'), hass, add_devices_callback) + + _CONFIGURING[host] = configurator.request_config( + hass, "Plex Media Server", plex_configuration_callback, + description=('Enter the X-Plex-Token'), + description_image="/static/images/config_plex_mediaserver.png", + submit_caption="Confirm", + fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}] + ) + + class PlexClient(MediaPlayerDevice): """ Represents a Plex device. """ - # pylint: disable=too-many-public-methods + # pylint: disable=too-many-public-methods, attribute-defined-outside-init def __init__(self, device, plex_sessions, update_devices, update_sessions): self.plex_sessions = plex_sessions self.update_devices = update_devices @@ -99,17 +199,23 @@ class PlexClient(MediaPlayerDevice): self.device = device @property - def session(self): - """ Returns the session, if any. """ - if self.device.clientIdentifier not in self.plex_sessions: - return None - - return self.plex_sessions[self.device.clientIdentifier] + def unique_id(self): + """ Returns the id of this plex client """ + return "{}.{}".format( + self.__class__, self.device.machineIdentifier or self.device.name) @property def name(self): """ Returns the name of the device. """ - return self.device.name or self.device.product or self.device.device + return self.device.name or DEVICE_DEFAULT_NAME + + @property + def session(self): + """ Returns the session, if any. """ + if self.device.machineIdentifier not in self.plex_sessions: + return None + + return self.plex_sessions[self.device.machineIdentifier] @property def state(self): @@ -120,7 +226,8 @@ class PlexClient(MediaPlayerDevice): return STATE_PLAYING elif state == 'paused': return STATE_PAUSED - elif self.device.isReachable: + # This is nasty. Need to find a way to determine alive + elif self.device: return STATE_IDLE else: return STATE_OFF @@ -196,16 +303,16 @@ class PlexClient(MediaPlayerDevice): def media_play(self): """ media_play media player. """ - self.device.play({'type': 'video'}) + self.device.play() def media_pause(self): """ media_pause media player. """ - self.device.pause({'type': 'video'}) + self.device.pause() def media_next_track(self): """ Send next track command. """ - self.device.skipNext({'type': 'video'}) + self.device.skipNext() def media_previous_track(self): """ Send previous track command. """ - self.device.skipPrevious({'type': 'video'}) + self.device.skipPrevious() diff --git a/requirements_all.txt b/requirements_all.txt index c63eea25853..11d91043d12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -134,7 +134,7 @@ https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5 SoCo==0.11.1 # PlexAPI (media_player.plex) -https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2 +plexapi==1.1.0 # SNMP (device_tracker.snmp) pysnmp==4.2.5