]> git.basschouten.com Git - openhab-addons.git/blob
ca6a93ff1c87f6754758f1a5490e9916aaad5c2b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.plex.internal.handler;
14
15 import static org.openhab.binding.plex.internal.PlexBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.util.ArrayList;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
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;
47
48 /**
49  * The {@link PlexServerHandler} is responsible for creating the
50  * Bridge Thing for a PLEX Server.
51  *
52  * @author Brian Homeyer - Initial contribution
53  * @author Aron Beurskens - Binding development
54  */
55 @NonNullByDefault
56 public class PlexServerHandler extends BaseBridgeHandler implements PlexUpdateListener {
57     private final Logger logger = LoggerFactory.getLogger(PlexServerHandler.class);
58
59     private final HttpClientFactory httpClientFactory;
60     private @Nullable HttpClient httpClient;
61
62     // Maintain mapping of handler and players
63     private final Map<String, PlexPlayerHandler> playerHandlers = new ConcurrentHashMap<>();
64
65     private PlexServerConfiguration config = new PlexServerConfiguration();
66     private PlexApiConnector plexAPIConnector;
67
68     private @Nullable ScheduledFuture<?> pollingJob;
69
70     private volatile boolean isRunning = false;
71
72     public PlexServerHandler(Bridge bridge, HttpClientFactory httpClientFactory) {
73         super(bridge);
74         this.httpClientFactory = httpClientFactory;
75         plexAPIConnector = new PlexApiConnector(scheduler, httpClientFactory.getCommonHttpClient());
76         logger.debug("Initializing server handler");
77     }
78
79     public PlexApiConnector getPlexAPIConnector() {
80         return plexAPIConnector;
81     }
82
83     /**
84      * Initialize the Bridge set the config paramaters for the PLEX Server and
85      * start the refresh Job.
86      */
87     @Override
88     public void initialize() {
89         final String httpClientName = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
90         try {
91             SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
92             sslContextFactory.setEndpointIdentificationAlgorithm(null);
93             HttpClient localHttpClient = httpClient = httpClientFactory.createHttpClient(httpClientName,
94                     sslContextFactory);
95             localHttpClient.start();
96             plexAPIConnector = new PlexApiConnector(scheduler, localHttpClient);
97         } catch (Exception e) {
98             logger.error(
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);
102             return;
103         }
104
105         config = getConfigAs(PlexServerConfiguration.class);
106         if (!config.host.isEmpty()) { // Check if a hostname is set
107             plexAPIConnector.setParameters(config);
108         } else {
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
110                     "Host must be specified, check configuration");
111             return;
112         }
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. ");
119                 return;
120             } else {
121                 try {
122                     plexAPIConnector.getToken();
123                 } catch (ConfigurationException e) {
124                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
125                     return;
126                 } catch (Exception e) {
127                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
128                     return;
129                 }
130             }
131         }
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?");
136             return;
137         }
138         isRunning = true;
139         onUpdate(); // Start the session refresh
140         scheduler.execute(() -> { // Start the web socket
141             synchronized (this) {
142                 if (isRunning) {
143                     final HttpClient localHttpClient = this.httpClient;
144                     if (localHttpClient != null) {
145                         PlexApiConnector localSockets = plexAPIConnector = new PlexApiConnector(scheduler,
146                                 localHttpClient);
147                         localSockets.setParameters(config);
148                         localSockets.registerListener(this);
149                         localSockets.connect();
150                     }
151                 }
152             }
153         });
154     }
155
156     /**
157      * Not currently used, all channels in this thing are read-only.
158      */
159     @Override
160     public void handleCommand(ChannelUID channelUID, Command command) {
161         return;
162     }
163
164     /**
165      * Gets a list of all the players currently being used w/ a status of local. This
166      * is used for discovery only.
167      *
168      * @return
169      */
170     public List<String> getAvailablePlayers() {
171         List<String> availablePlayers = new ArrayList<String>();
172         MediaContainer sessionData = plexAPIConnector.getSessionData();
173
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());
179                     }
180                 }
181             }
182         }
183         return availablePlayers;
184     }
185
186     /**
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.
189      */
190     @Override
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);
195     }
196
197     /**
198      * Called when a player has been removed from the system.
199      */
200     @Override
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);
205     }
206
207     /**
208      * Basically a callback method for the websocket handling
209      */
210     @Override
211     public void onItemStatusUpdate(String sessionKey, String state) {
212         try {
213             for (Map.Entry<String, PlexPlayerHandler> entry : playerHandlers.entrySet()) {
214                 if (entry.getValue().getSessionKey().equals(sessionKey)) {
215                     entry.getValue().updateStateChannel(state);
216                 }
217             }
218         } catch (Exception e) {
219             logger.debug("Failed setting item status : {}", e.getMessage());
220         }
221     }
222
223     /**
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.
227      *
228      * @param sessionData The MediaContainer object that is pulled from the XML result of
229      *            a call to the session data on PLEX.
230      */
231     @SuppressWarnings("null")
232     private void refreshStates(MediaContainer sessionData) {
233         int playerCount = 0;
234         int playerActiveCount = 0;
235         Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
236         while (valueIterator.hasNext()) {
237             playerCount++;
238             valueIterator.next().setFoundInSession(false);
239         }
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
245                                                                                               // it
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());
254                     } else {
255                         tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
256                     }
257                     playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()).refreshSessionData(tmpMeta);
258                     playerActiveCount++;
259                 }
260             }
261         }
262
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)));
267     }
268
269     /**
270      * Refresh all the configured players
271      */
272     private void refreshAllPlayers() {
273         Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
274         while (valueIterator.hasNext()) {
275             valueIterator.next().updateChannels();
276         }
277     }
278
279     /**
280      * This is called to start the refresh job and also to reset that refresh job when a config change is done.
281      */
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);
287         }
288     }
289
290     /**
291      * The refresh job, pulls the session data and then calls refreshAllPlayers which will have them send
292      * out their current status.
293      */
294     private Runnable pollingRunnable = () -> {
295         try {
296             MediaContainer plexSessionData = plexAPIConnector.getSessionData();
297             if (plexSessionData != null) {
298                 refreshStates(plexSessionData);
299                 updateStatus(ThingStatus.ONLINE);
300             } else {
301                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
302                         "PLEX is not returning valid session data");
303             }
304             refreshAllPlayers();
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());
308         }
309     };
310
311     @Override
312     public void dispose() {
313         logger.debug("Disposing PLEX Bridge Handler.");
314         isRunning = false;
315         plexAPIConnector.dispose();
316
317         ScheduledFuture<?> pollingJob = this.pollingJob;
318         if (pollingJob != null && !pollingJob.isCancelled()) {
319             pollingJob.cancel(true);
320             this.pollingJob = null;
321         }
322     }
323 }