* Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless.
260 lines
8.0 KiB
Python
260 lines
8.0 KiB
Python
"""Support to embed Plex."""
|
|
import asyncio
|
|
import functools
|
|
import json
|
|
import logging
|
|
|
|
import plexapi.exceptions
|
|
from plexwebsocket import PlexWebsocket
|
|
import requests.exceptions
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
|
from homeassistant.components.media_player.const import (
|
|
ATTR_MEDIA_CONTENT_ID,
|
|
ATTR_MEDIA_CONTENT_TYPE,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
CONF_URL,
|
|
CONF_VERIFY_SSL,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_connect,
|
|
async_dispatcher_send,
|
|
)
|
|
|
|
from .const import (
|
|
CONF_SERVER,
|
|
CONF_SERVER_IDENTIFIER,
|
|
DISPATCHERS,
|
|
DOMAIN as PLEX_DOMAIN,
|
|
PLATFORMS,
|
|
PLATFORMS_COMPLETED,
|
|
PLEX_SERVER_CONFIG,
|
|
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
|
SERVERS,
|
|
SERVICE_PLAY_ON_SONOS,
|
|
WEBSOCKETS,
|
|
)
|
|
from .errors import ShouldUpdateConfigEntry
|
|
from .server import PlexServer
|
|
|
|
_LOGGER = logging.getLogger(__package__)
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
"""Set up the Plex component."""
|
|
hass.data.setdefault(
|
|
PLEX_DOMAIN,
|
|
{SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}},
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass, entry):
|
|
"""Set up Plex from a config entry."""
|
|
server_config = entry.data[PLEX_SERVER_CONFIG]
|
|
|
|
if entry.unique_id is None:
|
|
hass.config_entries.async_update_entry(
|
|
entry, unique_id=entry.data[CONF_SERVER_IDENTIFIER]
|
|
)
|
|
|
|
if MP_DOMAIN not in entry.options:
|
|
options = dict(entry.options)
|
|
options.setdefault(MP_DOMAIN, {})
|
|
hass.config_entries.async_update_entry(entry, options=options)
|
|
|
|
plex_server = PlexServer(
|
|
hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options
|
|
)
|
|
try:
|
|
await hass.async_add_executor_job(plex_server.connect)
|
|
except ShouldUpdateConfigEntry:
|
|
new_server_data = {
|
|
**entry.data[PLEX_SERVER_CONFIG],
|
|
CONF_URL: plex_server.url_in_use,
|
|
CONF_SERVER: plex_server.friendly_name,
|
|
}
|
|
hass.config_entries.async_update_entry(
|
|
entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data}
|
|
)
|
|
except requests.exceptions.ConnectionError as error:
|
|
_LOGGER.error(
|
|
"Plex server (%s) could not be reached: [%s]",
|
|
server_config[CONF_URL],
|
|
error,
|
|
)
|
|
raise ConfigEntryNotReady from error
|
|
except (
|
|
plexapi.exceptions.BadRequest,
|
|
plexapi.exceptions.Unauthorized,
|
|
plexapi.exceptions.NotFound,
|
|
) as error:
|
|
_LOGGER.error(
|
|
"Login to %s failed, verify token and SSL settings: [%s]",
|
|
entry.data[CONF_SERVER],
|
|
error,
|
|
)
|
|
return False
|
|
|
|
_LOGGER.debug(
|
|
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
|
|
)
|
|
server_id = plex_server.machine_identifier
|
|
hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server
|
|
hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set()
|
|
|
|
entry.add_update_listener(async_options_updated)
|
|
|
|
unsub = async_dispatcher_connect(
|
|
hass,
|
|
PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id),
|
|
plex_server.async_update_platforms,
|
|
)
|
|
hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, [])
|
|
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
|
|
|
|
def update_plex():
|
|
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
|
|
|
session = async_get_clientsession(hass)
|
|
verify_ssl = server_config.get(CONF_VERIFY_SSL)
|
|
websocket = PlexWebsocket(
|
|
plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl
|
|
)
|
|
hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket
|
|
|
|
def start_websocket_session(platform, _):
|
|
hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform)
|
|
if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS:
|
|
hass.loop.create_task(websocket.listen())
|
|
|
|
def close_websocket_session(_):
|
|
websocket.close()
|
|
|
|
unsub = hass.bus.async_listen_once(
|
|
EVENT_HOMEASSISTANT_STOP, close_websocket_session
|
|
)
|
|
hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
|
|
|
|
for platform in PLATFORMS:
|
|
task = hass.async_create_task(
|
|
hass.config_entries.async_forward_entry_setup(entry, platform)
|
|
)
|
|
task.add_done_callback(functools.partial(start_websocket_session, platform))
|
|
|
|
async def async_play_on_sonos_service(service_call):
|
|
await hass.async_add_executor_job(play_on_sonos, hass, service_call)
|
|
|
|
play_on_sonos_schema = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
|
vol.Required(ATTR_MEDIA_CONTENT_ID): str,
|
|
vol.Optional(ATTR_MEDIA_CONTENT_TYPE): vol.In("music"),
|
|
}
|
|
)
|
|
|
|
def get_plex_account(plex_server):
|
|
try:
|
|
return plex_server.account
|
|
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized):
|
|
return None
|
|
|
|
plex_account = await hass.async_add_executor_job(get_plex_account, plex_server)
|
|
if plex_account:
|
|
hass.services.async_register(
|
|
PLEX_DOMAIN,
|
|
SERVICE_PLAY_ON_SONOS,
|
|
async_play_on_sonos_service,
|
|
schema=play_on_sonos_schema,
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
"""Unload a config entry."""
|
|
server_id = entry.data[CONF_SERVER_IDENTIFIER]
|
|
|
|
websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id)
|
|
websocket.close()
|
|
|
|
dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id)
|
|
for unsub in dispatchers:
|
|
unsub()
|
|
|
|
tasks = [
|
|
hass.config_entries.async_forward_entry_unload(entry, platform)
|
|
for platform in PLATFORMS
|
|
]
|
|
await asyncio.gather(*tasks)
|
|
|
|
hass.data[PLEX_DOMAIN][SERVERS].pop(server_id)
|
|
|
|
return True
|
|
|
|
|
|
async def async_options_updated(hass, entry):
|
|
"""Triggered by config entry options updates."""
|
|
server_id = entry.data[CONF_SERVER_IDENTIFIER]
|
|
hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
|
|
|
|
|
|
def play_on_sonos(hass, service_call):
|
|
"""Play Plex media on a linked Sonos device."""
|
|
entity_id = service_call.data[ATTR_ENTITY_ID]
|
|
content_id = service_call.data[ATTR_MEDIA_CONTENT_ID]
|
|
content = json.loads(content_id)
|
|
|
|
sonos = hass.components.sonos
|
|
try:
|
|
sonos_name = sonos.get_coordinator_name(entity_id)
|
|
except HomeAssistantError as err:
|
|
_LOGGER.error("Cannot get Sonos device: %s", err)
|
|
return
|
|
|
|
if isinstance(content, int):
|
|
content = {"plex_key": content}
|
|
content_type = PLEX_DOMAIN
|
|
else:
|
|
content_type = "music"
|
|
|
|
plex_server_name = content.get("plex_server")
|
|
shuffle = content.pop("shuffle", 0)
|
|
|
|
plex_servers = hass.data[PLEX_DOMAIN][SERVERS].values()
|
|
if plex_server_name:
|
|
plex_server = [x for x in plex_servers if x.friendly_name == plex_server_name]
|
|
if not plex_server:
|
|
_LOGGER.error(
|
|
"Requested Plex server '%s' not found in %s",
|
|
plex_server_name,
|
|
list(map(lambda x: x.friendly_name, plex_servers)),
|
|
)
|
|
return
|
|
else:
|
|
plex_server = next(iter(plex_servers))
|
|
|
|
sonos_speaker = plex_server.account.sonos_speaker(sonos_name)
|
|
if sonos_speaker is None:
|
|
_LOGGER.error(
|
|
"Sonos speaker '%s' could not be found on this Plex account", sonos_name
|
|
)
|
|
return
|
|
|
|
media = plex_server.lookup_media(content_type, **content)
|
|
if media is None:
|
|
_LOGGER.error("Media could not be found: %s", content)
|
|
return
|
|
|
|
_LOGGER.debug("Attempting to play '%s' on %s", media, sonos_speaker)
|
|
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
|
|
sonos_speaker.playMedia(playqueue)
|