]> git.basschouten.com Git - openhab-addons.git/blob
6f86e2b8ccbaa2bb1a220e0bcd787940b3e583de
[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             sslContextFactory.setTrustAll(true);
94             HttpClient localHttpClient = httpClient = httpClientFactory.createHttpClient(httpClientName,
95                     sslContextFactory);
96             localHttpClient.start();
97             plexAPIConnector = new PlexApiConnector(scheduler, localHttpClient);
98         } catch (Exception e) {
99             logger.error(
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);
103             return;
104         }
105
106         config = getConfigAs(PlexServerConfiguration.class);
107         if (!config.host.isEmpty()) { // Check if a hostname is set
108             plexAPIConnector.setParameters(config);
109         } else {
110             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111                     "Host must be specified, check configuration");
112             return;
113         }
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. ");
120                 return;
121             } else {
122                 try {
123                     plexAPIConnector.getToken();
124                 } catch (ConfigurationException e) {
125                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
126                     return;
127                 } catch (Exception e) {
128                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
129                     return;
130                 }
131             }
132         }
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?");
137             return;
138         }
139         isRunning = true;
140         onUpdate(); // Start the session refresh
141         scheduler.execute(() -> { // Start the web socket
142             synchronized (this) {
143                 if (isRunning) {
144                     final HttpClient localHttpClient = this.httpClient;
145                     if (localHttpClient != null) {
146                         PlexApiConnector localSockets = plexAPIConnector = new PlexApiConnector(scheduler,
147                                 localHttpClient);
148                         localSockets.setParameters(config);
149                         localSockets.registerListener(this);
150                         localSockets.connect();
151                     }
152                 }
153             }
154         });
155     }
156
157     /**
158      * Not currently used, all channels in this thing are read-only.
159      */
160     @Override
161     public void handleCommand(ChannelUID channelUID, Command command) {
162         return;
163     }
164
165     /**
166      * Gets a list of all the players currently being used w/ a status of local. This
167      * is used for discovery only.
168      *
169      * @return
170      */
171     public List<String> getAvailablePlayers() {
172         List<String> availablePlayers = new ArrayList<String>();
173         MediaContainer sessionData = plexAPIConnector.getSessionData();
174
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());
180                     }
181                 }
182             }
183         }
184         return availablePlayers;
185     }
186
187     /**
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.
190      */
191     @Override
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);
196     }
197
198     /**
199      * Called when a player has been removed from the system.
200      */
201     @Override
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);
206     }
207
208     /**
209      * Basically a callback method for the websocket handling
210      */
211     @Override
212     public void onItemStatusUpdate(String sessionKey, String state) {
213         try {
214             for (Map.Entry<String, PlexPlayerHandler> entry : playerHandlers.entrySet()) {
215                 if (entry.getValue().getSessionKey().equals(sessionKey)) {
216                     entry.getValue().updateStateChannel(state);
217                 }
218             }
219         } catch (Exception e) {
220             logger.debug("Failed setting item status : {}", e.getMessage());
221         }
222     }
223
224     /**
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.
228      *
229      * @param sessionData The MediaContainer object that is pulled from the XML result of
230      *            a call to the session data on PLEX.
231      */
232     @SuppressWarnings("null")
233     private void refreshStates(MediaContainer sessionData) {
234         int playerCount = 0;
235         int playerActiveCount = 0;
236         Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
237         while (valueIterator.hasNext()) {
238             playerCount++;
239             valueIterator.next().setFoundInSession(false);
240         }
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
246                                                                                               // it
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());
255                     } else {
256                         tmpMeta.setThumb(plexAPIConnector.getURL(tmpMeta.getThumb()));
257                     }
258                     playerHandlers.get(tmpMeta.getPlayer().getMachineIdentifier()).refreshSessionData(tmpMeta);
259                     playerActiveCount++;
260                 }
261             }
262         }
263
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)));
268     }
269
270     /**
271      * Refresh all the configured players
272      */
273     private void refreshAllPlayers() {
274         Iterator<PlexPlayerHandler> valueIterator = playerHandlers.values().iterator();
275         while (valueIterator.hasNext()) {
276             valueIterator.next().updateChannels();
277         }
278     }
279
280     /**
281      * This is called to start the refresh job and also to reset that refresh job when a config change is done.
282      */
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);
288         }
289     }
290
291     /**
292      * The refresh job, pulls the session data and then calls refreshAllPlayers which will have them send
293      * out their current status.
294      */
295     private Runnable pollingRunnable = () -> {
296         try {
297             MediaContainer plexSessionData = plexAPIConnector.getSessionData();
298             if (plexSessionData != null) {
299                 refreshStates(plexSessionData);
300                 updateStatus(ThingStatus.ONLINE);
301             } else {
302                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
303                         "PLEX is not returning valid session data");
304             }
305             refreshAllPlayers();
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()));
309         }
310     };
311
312     @Override
313     public void dispose() {
314         logger.debug("Disposing PLEX Bridge Handler.");
315         isRunning = false;
316         plexAPIConnector.dispose();
317
318         ScheduledFuture<?> pollingJob = this.pollingJob;
319         if (pollingJob != null && !pollingJob.isCancelled()) {
320             pollingJob.cancel(true);
321             this.pollingJob = null;
322         }
323     }
324 }