1 """Support to embed Plex."""
3 from functools
import partial
6 import plexapi.exceptions
7 from plexapi.gdm
import GDM
8 from plexwebsocket
import (
9 SIGNAL_CONNECTION_STATE,
15 import requests.exceptions
23 config_validation
as cv,
24 device_registry
as dr,
25 entity_registry
as er,
30 async_dispatcher_connect,
31 async_dispatcher_send,
40 CONF_SERVER_IDENTIFIER,
43 INVALID_TOKEN_MESSAGE,
46 PLEX_UPDATE_LIBRARY_SIGNAL,
47 PLEX_UPDATE_PLATFORMS_SIGNAL,
52 from .errors
import ShouldUpdateConfigEntry
53 from .helpers
import PlexData, get_plex_data
54 from .media_browser
import browse_media
55 from .server
import PlexServer
56 from .services
import async_setup_services
57 from .view
import PlexImageView
59 _LOGGER = logging.getLogger(__package__)
61 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
65 """Return whether the media_content_id is a valid Plex media_id."""
66 return media_content_id
and media_content_id.startswith(PLEX_URI_SCHEME)
70 """Browse Plex media."""
71 plex_server = next(iter(
get_plex_data(hass)[SERVERS].values()),
None)
73 raise BrowseError(
"No Plex servers available")
75 return await hass.async_add_executor_job(
87 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
88 """Set up the Plex component."""
92 _LOGGER.debug(
"Scanning for GDM clients")
93 gdm.scan(scan_for_clients=
True)
95 debouncer = Debouncer[
None](
96 hass, _LOGGER, cooldown=10, immediate=
True, function=gdm_scan, background=
True
104 gdm_debouncer=debouncer,
106 hass.data.setdefault(DOMAIN, hass_data)
116 """Set up Plex from a config entry."""
117 server_config = entry.data[PLEX_SERVER_CONFIG]
119 if entry.unique_id
is None:
120 hass.config_entries.async_update_entry(
121 entry, unique_id=entry.data[CONF_SERVER_IDENTIFIER]
124 if MP_DOMAIN
not in entry.options:
125 options =
dict(entry.options)
126 options.setdefault(MP_DOMAIN, {})
127 hass.config_entries.async_update_entry(entry, options=options)
132 entry.data[CONF_SERVER_IDENTIFIER],
137 await hass.async_add_executor_job(plex_server.connect)
138 except ShouldUpdateConfigEntry:
140 **entry.data[PLEX_SERVER_CONFIG],
141 CONF_URL: plex_server.url_in_use,
142 CONF_SERVER: plex_server.friendly_name,
144 hass.config_entries.async_update_entry(
145 entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data}
147 except requests.exceptions.ConnectionError
as error:
148 raise ConfigEntryNotReady
from error
149 except plexapi.exceptions.Unauthorized
as ex:
151 "Token not accepted, please reauthenticate Plex server"
152 f
" '{entry.data[CONF_SERVER]}'"
155 plexapi.exceptions.BadRequest,
156 plexapi.exceptions.NotFound,
158 if INVALID_TOKEN_MESSAGE
in str(error):
160 "Token not accepted, please reauthenticate Plex server"
161 f
" '{entry.data[CONF_SERVER]}'"
164 "Login to %s failed, verify token and SSL settings: [%s]",
165 entry.data[CONF_SERVER],
169 raise ConfigEntryNotReady
from error
172 "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
174 server_id = plex_server.machine_identifier
176 hass_data[SERVERS][server_id] = plex_server
178 entry.add_update_listener(async_options_updated)
182 PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id),
183 plex_server.async_update_platforms,
185 hass_data[DISPATCHERS].setdefault(server_id, [])
186 hass_data[DISPATCHERS][server_id].append(unsub)
189 def plex_websocket_callback(msgtype, data, error):
190 """Handle callbacks from plexwebsocket library."""
191 if msgtype == SIGNAL_CONNECTION_STATE:
192 if data == STATE_CONNECTED:
193 _LOGGER.debug(
"Websocket to %s successful", entry.data[CONF_SERVER])
194 hass.async_create_task(plex_server.async_update_platforms())
195 elif data == STATE_DISCONNECTED:
197 "Websocket to %s disconnected, retrying", entry.data[CONF_SERVER]
200 elif data == STATE_STOPPED
and error:
202 "Websocket to %s failed, aborting [Error: %s]",
203 entry.data[CONF_SERVER],
206 hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
208 elif msgtype ==
"playing":
209 hass.async_create_task(plex_server.async_update_session(data))
210 elif msgtype ==
"status":
211 if data[
"StatusNotification"][0][
"title"] ==
"Library scan complete":
214 PLEX_UPDATE_LIBRARY_SIGNAL.format(server_id),
218 subscriptions = [
"playing",
"status"]
219 verify_ssl = server_config.get(CONF_VERIFY_SSL)
220 websocket = PlexWebsocket(
221 plex_server.plex_server,
222 plex_websocket_callback,
223 subscriptions=subscriptions,
225 verify_ssl=verify_ssl,
227 hass_data[WEBSOCKETS][server_id] = websocket
229 def close_websocket_session(_):
232 unsub = hass.bus.async_listen_once(
233 EVENT_HOMEASSISTANT_STOP, close_websocket_session
235 hass_data[DISPATCHERS][server_id].append(unsub)
237 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
239 entry.async_create_background_task(
240 hass, websocket.listen(), f
"plex websocket listener {entry.entry_id}"
245 def get_plex_account(plex_server):
247 return plex_server.account
248 except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized):
251 await hass.async_add_executor_job(get_plex_account, plex_server)
254 def scheduled_client_scan(_):
255 _LOGGER.debug(
"Scheduled scan for new clients on %s", plex_server.friendly_name)
258 entry.async_on_unload(
261 scheduled_client_scan,
262 CLIENT_SCAN_INTERVAL,
270 """Unload a config entry."""
271 server_id = entry.data[CONF_SERVER_IDENTIFIER]
274 websocket = hass_data[WEBSOCKETS].pop(server_id)
277 dispatchers = hass_data[DISPATCHERS].pop(server_id)
278 for unsub
in dispatchers:
281 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
283 hass_data[SERVERS].pop(server_id)
289 """Triggered by config entry options updates."""
290 server_id = entry.data[CONF_SERVER_IDENTIFIER]
294 if server_id
in hass_data[SERVERS]:
295 hass_data[SERVERS][server_id].options = entry.options
300 """Clean up old and invalid devices from the registry."""
301 device_registry = dr.async_get(hass)
302 entity_registry = er.async_get(hass)
304 device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
306 for device_entry
in device_entries:
309 er.async_entries_for_device(
310 entity_registry, device_entry.id, include_disabled_entities=
True
316 "Removing orphaned device: %s / %s",
318 device_entry.identifiers,
320 device_registry.async_remove_device(device_entry.id)
None async_setup_services(HomeAssistant hass)
PlexData get_plex_data(HomeAssistant hass)
def is_plex_media_id(media_content_id)
def async_browse_media(hass, media_content_type, media_content_id, platform=None)
def async_cleanup_plex_devices(hass, entry)
None async_options_updated(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
bool is_internal_request(HomeAssistant hass)