2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.plex.internal.handler;
15 import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.Iterator;
20 import java.util.List;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.util.ssl.SslContextFactory;
30 import org.openhab.binding.plex.internal.config.PlexServerConfiguration;
31 import org.openhab.binding.plex.internal.dto.MediaContainer;
32 import org.openhab.binding.plex.internal.dto.MediaContainer.MediaType;
33 import org.openhab.core.i18n.ConfigurationException;
34 import org.openhab.core.io.net.http.HttpClientFactory;
35 import org.openhab.core.library.types.StringType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.thing.util.ThingWebClientUtil;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link PlexServerHandler} is responsible for creating the
50 * Bridge Thing for a PLEX Server.
52 * @author Brian Homeyer - Initial contribution
53 * @author Aron Beurskens - Binding development
56 public class PlexServerHandler extends BaseBridgeHandler implements PlexUpdateListener {
57 private final Logger logger = LoggerFactory.getLogger(PlexServerHandler.class);
59 private final HttpClientFactory httpClientFactory;
60 private @Nullable HttpClient httpClient;
62 // Maintain mapping of handler and players
63 private final Map<String, PlexPlayerHandler> playerHandlers = new ConcurrentHashMap<>();
65 private PlexServerConfiguration config = new PlexServerConfiguration();
66 private PlexApiConnector plexAPIConnector;
68 private @Nullable ScheduledFuture<?> pollingJob;
70 private volatile boolean isRunning = false;
72 public PlexServerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
74 this.httpClientFactory = httpClientFactory;
75 plexAPIConnector = new PlexApiConnector(scheduler, httpClientFactory.getCommonHttpClient());
76 logger.debug("Initializing server handler");
79 public PlexApiConnector getPlexAPIConnector() {
80 return plexAPIConnector;
84 * Initialize the Bridge set the config paramaters for the PLEX Server and
85 * start the refresh Job.
88 public void initialize() {
89 final String httpClientName = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
91 SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
92 sslContextFactory.setEndpointIdentificationAlgorithm(null);
93 sslContextFactory.setTrustAll(true);
94 HttpClient localHttpClient = httpClient = httpClientFactory.createHttpClient(httpClientName,
96 localHttpClient.start();
97 plexAPIConnector = new PlexApiConnector(scheduler, localHttpClient);
98 } catch (Exception e) {
100 "Long running HttpClient for PlexServerHandler {} cannot be started. Creating Handler failed. Exception: {}",
101 httpClientName, e.getMessage(), e);
102 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
106 config = getConfigAs(PlexServerConfiguration.class);
107 if (!config.host.isEmpty()) { // Check if a hostname is set
108 plexAPIConnector.setParameters(config);
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111 "Host must be specified, check configuration");
114 if (!plexAPIConnector.hasToken()) {
115 // No token is set by config, let's see if we can fetch one from username/password
116 logger.debug("Token is not set, trying to fetch one");
117 if (config.username.isEmpty() || config.password.isEmpty()) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119 "Username, password and Token is not set, unable to connect to PLEX without. ");
123 plexAPIConnector.getToken();
124 } catch (ConfigurationException e) {
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
127 } catch (Exception e) {
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
133 logger.debug("Fetch API with config, {}", config.toString());
134 if (!plexAPIConnector.getApi()) {
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
136 "Unable to fetch API, token may be wrong?");
140 onUpdate(); // Start the session refresh
141 scheduler.execute(() -> { // Start the web socket
142 synchronized (this) {
144 final HttpClient localHttpClient = this.httpClient;
145 if (localHttpClient != null) {
146 PlexApiConnector localSockets = plexAPIConnector = new PlexApiConnector(scheduler,
148 localSockets.setParameters(config);
149 localSockets.registerListener(this);
150 localSockets.connect();
158 * Not currently used, all channels in this thing are read-only.
161 public void handleCommand(ChannelUID channelUID, Command command) {
166 * Gets a list of all the players currently being used w/ a status of local. This
167 * is used for discovery only.
171 public List<String> getAvailablePlayers() {
172 List<String> availablePlayers = new ArrayList<String>();
173 MediaContainer sessionData = plexAPIConnector.getSessionData();
175 if (sessionData != null && sessionData.getSize() > 0) {
176 for (MediaType tmpMeta : sessionData.getMediaTypes()) {
177 if (tmpMeta != null && playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) == null) {
178 if ("1".equals(tmpMeta.getPlayer().getLocal())) {
179 availablePlayers.add(tmpMeta.getPlayer().getMachineIdentifier());
184 return availablePlayers;
188 * Called when a new player thing has been added. We add it to the hash map so we can
189 * keep track of things.
192 public synchronized void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
193 String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
194 playerHandlers.put(playerID, (PlexPlayerHandler) childHandler);
195 logger.debug("Bridge: Monitor handler was initialized for {} with id {}", childThing.getUID(), playerID);
199 * Called when a player has been removed from the system.
202 public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
203 String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
204 playerHandlers.remove(playerID);
205 logger.debug("Bridge: Monitor handler was disposed for {} with id {}", childThing.getUID(), playerID);
209 * Basically a callback method for the websocket handling
212 public void onItemStatusUpdate(String sessionKey, String state) {
214 for (Map.Entry<String, PlexPlayerHandler> entry : playerHandlers.entrySet()) {
215 if (entry.getValue().getSessionKey().equals(sessionKey)) {
216 entry.getValue().updateStateChannel(state);
219 } catch (Exception e) {
220 logger.debug("Failed setting item status : {}", e.getMessage());
225 * Clears the foundInSession field for the configured players, then it sets the
226 * data for the machineIds that are found in the session data set. This allows
227 * us to determine if a device is on or off.
229 * @param sessionData The MediaContainer object that is pulled from the XML result of
230 * a call to the session data on PLEX.
232 @SuppressWarnings("null")
233 private void refreshStates(MediaContainer sessionData) {
235 int playerActiveCount = 0;
236 Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
237 while (valueIterator.hasNext()) {
239 valueIterator.next().setFoundInSession(false);
241 if (sessionData != null && sessionData.getSize() > 0) { // Cover condition where nothing is playing
242 for (MediaContainer.MediaType tmpMeta : sessionData.getMediaTypes()) { // Roll through mediaType objects
243 // looking for machineID
244 if (playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) != null) { // if we have a player
245 // configured, update
247 tmpMeta.setArt(plexAPIConnector.getURL(tmpMeta.getArt()));
248 if ("episode".equals(tmpMeta.getType())) {
249 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getGrandparentThumb()));
250 tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " : " + tmpMeta.getTitle());
251 } else if ("track".equals(tmpMeta.getType())) {
252 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
253 tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " - " + tmpMeta.getParentTitle() + " - "
254 + tmpMeta.getTitle());
256 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
258 playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()).refreshSessionData(tmpMeta);
264 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNT),
265 new StringType(String.valueOf(playerCount)));
266 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNTACTIVE),
267 new StringType(String.valueOf(playerActiveCount)));
271 * Refresh all the configured players
273 private void refreshAllPlayers() {
274 Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
275 while (valueIterator.hasNext()) {
276 valueIterator.next().updateChannels();
281 * This is called to start the refresh job and also to reset that refresh job when a config change is done.
283 private synchronized void onUpdate() {
284 ScheduledFuture<?> pollingJob = this.pollingJob;
285 if (pollingJob == null || pollingJob.isCancelled()) {
286 int pollingInterval = ((BigDecimal) getConfig().get(CONFIG_REFRESH_RATE)).intValue();
287 this.pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 1, pollingInterval, TimeUnit.SECONDS);
292 * The refresh job, pulls the session data and then calls refreshAllPlayers which will have them send
293 * out their current status.
295 private Runnable pollingRunnable = () -> {
297 MediaContainer plexSessionData = plexAPIConnector.getSessionData();
298 if (plexSessionData != null) {
299 refreshStates(plexSessionData);
300 updateStatus(ThingStatus.ONLINE);
302 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
303 "PLEX is not returning valid session data");
306 } catch (Exception e) {
307 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
308 String.format("An exception occurred while polling the PLEX Server: '%s'", e.getMessage()));
313 public void dispose() {
314 logger.debug("Disposing PLEX Bridge Handler.");
316 plexAPIConnector.dispose();
318 ScheduledFuture<?> pollingJob = this.pollingJob;
319 if (pollingJob != null && !pollingJob.isCancelled()) {
320 pollingJob.cancel(true);
321 this.pollingJob = null;