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 HttpClient localHttpClient = httpClient = httpClientFactory.createHttpClient(httpClientName,
95 localHttpClient.start();
96 plexAPIConnector = new PlexApiConnector(scheduler, localHttpClient);
97 } catch (Exception e) {
99 "Long running HttpClient for PlexServerHandler {} cannot be started. Creating Handler failed. Exception: {}",
100 httpClientName, e.getMessage(), e);
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
105 config = getConfigAs(PlexServerConfiguration.class);
106 if (!config.host.isEmpty()) { // Check if a hostname is set
107 plexAPIConnector.setParameters(config);
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110 "Host must be specified, check configuration");
113 if (!plexAPIConnector.hasToken()) {
114 // No token is set by config, let's see if we can fetch one from username/password
115 logger.debug("Token is not set, trying to fetch one");
116 if (config.username.isEmpty() || config.password.isEmpty()) {
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
118 "Username, password and Token is not set, unable to connect to PLEX without. ");
122 plexAPIConnector.getToken();
123 } catch (ConfigurationException e) {
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
126 } catch (Exception e) {
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
132 logger.debug("Fetch API with config, {}", config.toString());
133 if (!plexAPIConnector.getApi()) {
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
135 "Unable to fetch API, token may be wrong?");
139 onUpdate(); // Start the session refresh
140 scheduler.execute(() -> { // Start the web socket
141 synchronized (this) {
143 final HttpClient localHttpClient = this.httpClient;
144 if (localHttpClient != null) {
145 PlexApiConnector localSockets = plexAPIConnector = new PlexApiConnector(scheduler,
147 localSockets.setParameters(config);
148 localSockets.registerListener(this);
149 localSockets.connect();
157 * Not currently used, all channels in this thing are read-only.
160 public void handleCommand(ChannelUID channelUID, Command command) {
165 * Gets a list of all the players currently being used w/ a status of local. This
166 * is used for discovery only.
170 public List<String> getAvailablePlayers() {
171 List<String> availablePlayers = new ArrayList<String>();
172 MediaContainer sessionData = plexAPIConnector.getSessionData();
174 if (sessionData != null && sessionData.getSize() > 0) {
175 for (MediaType tmpMeta : sessionData.getMediaTypes()) {
176 if (tmpMeta != null && playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) == null) {
177 if ("1".equals(tmpMeta.getPlayer().getLocal())) {
178 availablePlayers.add(tmpMeta.getPlayer().getMachineIdentifier());
183 return availablePlayers;
187 * Called when a new player thing has been added. We add it to the hash map so we can
188 * keep track of things.
191 public synchronized void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
192 String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
193 playerHandlers.put(playerID, (PlexPlayerHandler) childHandler);
194 logger.debug("Bridge: Monitor handler was initialized for {} with id {}", childThing.getUID(), playerID);
198 * Called when a player has been removed from the system.
201 public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
202 String playerID = (String) childThing.getConfiguration().get(CONFIG_PLAYER_ID);
203 playerHandlers.remove(playerID);
204 logger.debug("Bridge: Monitor handler was disposed for {} with id {}", childThing.getUID(), playerID);
208 * Basically a callback method for the websocket handling
211 public void onItemStatusUpdate(String sessionKey, String state) {
213 for (Map.Entry<String, PlexPlayerHandler> entry : playerHandlers.entrySet()) {
214 if (entry.getValue().getSessionKey().equals(sessionKey)) {
215 entry.getValue().updateStateChannel(state);
218 } catch (Exception e) {
219 logger.debug("Failed setting item status : {}", e.getMessage());
224 * Clears the foundInSession field for the configured players, then it sets the
225 * data for the machineIds that are found in the session data set. This allows
226 * us to determine if a device is on or off.
228 * @param sessionData The MediaContainer object that is pulled from the XML result of
229 * a call to the session data on PLEX.
231 @SuppressWarnings("null")
232 private void refreshStates(MediaContainer sessionData) {
234 int playerActiveCount = 0;
235 Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
236 while (valueIterator.hasNext()) {
238 valueIterator.next().setFoundInSession(false);
240 if (sessionData != null && sessionData.getSize() > 0) { // Cover condition where nothing is playing
241 for (MediaContainer.MediaType tmpMeta : sessionData.getMediaTypes()) { // Roll through mediaType objects
242 // looking for machineID
243 if (playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()) != null) { // if we have a player
244 // configured, update
246 tmpMeta.setArt(plexAPIConnector.getURL(tmpMeta.getArt()));
247 if (tmpMeta.getType().equals("episode")) {
248 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getGrandparentThumb()));
249 tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " : " + tmpMeta.getTitle());
250 } else if (tmpMeta.getType().equals("track")) {
251 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
252 tmpMeta.setTitle(tmpMeta.getGrandparentTitle() + " - " + tmpMeta.getParentTitle() + " - "
253 + tmpMeta.getTitle());
255 tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
257 playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()).refreshSessionData(tmpMeta);
263 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNT),
264 new StringType(String.valueOf(playerCount)));
265 updateState(new ChannelUID(getThing().getUID(), CHANNEL_SERVER_COUNTACTIVE),
266 new StringType(String.valueOf(playerActiveCount)));
270 * Refresh all the configured players
272 private void refreshAllPlayers() {
273 Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
274 while (valueIterator.hasNext()) {
275 valueIterator.next().updateChannels();
280 * This is called to start the refresh job and also to reset that refresh job when a config change is done.
282 private synchronized void onUpdate() {
283 ScheduledFuture<?> pollingJob = this.pollingJob;
284 if (pollingJob == null || pollingJob.isCancelled()) {
285 int pollingInterval = ((BigDecimal) getConfig().get(CONFIG_REFRESH_RATE)).intValue();
286 this.pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 1, pollingInterval, TimeUnit.SECONDS);
291 * The refresh job, pulls the session data and then calls refreshAllPlayers which will have them send
292 * out their current status.
294 private Runnable pollingRunnable = () -> {
296 MediaContainer plexSessionData = plexAPIConnector.getSessionData();
297 if (plexSessionData != null) {
298 refreshStates(plexSessionData);
299 updateStatus(ThingStatus.ONLINE);
301 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
302 "PLEX is not returning valid session data");
305 } catch (Exception e) {
306 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, String
307 .format("An exception occurred while polling the PLEX Server: '%s'", e.getMessage()).toString());
312 public void dispose() {
313 logger.debug("Disposing PLEX Bridge Handler.");
315 plexAPIConnector.dispose();
317 ScheduledFuture<?> pollingJob = this.pollingJob;
318 if (pollingJob != null && !pollingJob.isCancelled()) {
319 pollingJob.cancel(true);
320 this.pollingJob = null;