]> git.basschouten.com Git - openhab-addons.git/blob
4949041273964e7daef3087c76f47dd74e7bc7a0
[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.kodi.internal.handler;
14
15 import static org.openhab.binding.kodi.internal.KodiBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.stream.Collectors;
25
26 import javax.measure.Unit;
27
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.websocket.client.WebSocketClient;
30 import org.openhab.binding.kodi.internal.KodiDynamicCommandDescriptionProvider;
31 import org.openhab.binding.kodi.internal.KodiDynamicStateDescriptionProvider;
32 import org.openhab.binding.kodi.internal.KodiEventListener;
33 import org.openhab.binding.kodi.internal.KodiPlayerState;
34 import org.openhab.binding.kodi.internal.config.KodiChannelConfig;
35 import org.openhab.binding.kodi.internal.config.KodiConfig;
36 import org.openhab.binding.kodi.internal.model.KodiAudioStream;
37 import org.openhab.binding.kodi.internal.model.KodiFavorite;
38 import org.openhab.binding.kodi.internal.model.KodiPVRChannel;
39 import org.openhab.binding.kodi.internal.model.KodiProfile;
40 import org.openhab.binding.kodi.internal.model.KodiSubtitle;
41 import org.openhab.binding.kodi.internal.model.KodiSystemProperties;
42 import org.openhab.binding.kodi.internal.protocol.KodiConnection;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.IncreaseDecreaseType;
45 import org.openhab.core.library.types.NextPreviousType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.PlayPauseType;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.library.types.RawType;
51 import org.openhab.core.library.types.RewindFastforwardType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.library.unit.Units;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.BaseThingHandler;
60 import org.openhab.core.thing.type.ChannelTypeUID;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.CommandOption;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.StateOption;
66 import org.openhab.core.types.UnDefType;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 /**
71  * The {@link KodiHandler} is responsible for handling commands, which are sent
72  * to one of the channels.
73  *
74  * @author Paul Frank - Initial contribution
75  * @author Christoph Weitkamp - Added channels for opening PVR TV or Radio streams
76  * @author Andreas Reinhardt & Christoph Weitkamp - Added channels for thumbnail and fanart
77  * @author Christoph Weitkamp - Improvements for playing audio notifications
78  * @author Meng Yiqi - Added selection of audio and subtitle
79  */
80 public class KodiHandler extends BaseThingHandler implements KodiEventListener {
81
82     private static final String SYSTEM_COMMAND_HIBERNATE = "Hibernate";
83     private static final String SYSTEM_COMMAND_REBOOT = "Reboot";
84     private static final String SYSTEM_COMMAND_SHUTDOWN = "Shutdown";
85     private static final String SYSTEM_COMMAND_SUSPEND = "Suspend";
86     private static final String SYSTEM_COMMAND_QUIT = "Quit";
87
88     private final Logger logger = LoggerFactory.getLogger(KodiHandler.class);
89
90     private final KodiConnection connection;
91     private final KodiDynamicCommandDescriptionProvider commandDescriptionProvider;
92     private final KodiDynamicStateDescriptionProvider stateDescriptionProvider;
93
94     private final ChannelUID screenSaverChannelUID;
95     private final ChannelUID inputRequestedChannelUID;
96     private final ChannelUID volumeChannelUID;
97     private final ChannelUID mutedChannelUID;
98     private final ChannelUID favoriteChannelUID;
99     private final ChannelUID profileChannelUID;
100
101     private ScheduledFuture<?> connectionCheckerFuture;
102     private ScheduledFuture<?> statusUpdaterFuture;
103
104     public KodiHandler(Thing thing, KodiDynamicCommandDescriptionProvider commandDescriptionProvider,
105             KodiDynamicStateDescriptionProvider stateDescriptionProvider, WebSocketClient webSocketClient,
106             String callbackUrl) {
107         super(thing);
108         connection = new KodiConnection(this, webSocketClient, callbackUrl);
109
110         this.commandDescriptionProvider = commandDescriptionProvider;
111         this.stateDescriptionProvider = stateDescriptionProvider;
112
113         screenSaverChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_SCREENSAVER);
114         inputRequestedChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_INPUTREQUESTED);
115         volumeChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_VOLUME);
116         mutedChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_MUTE);
117         favoriteChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_PLAYFAVORITE);
118         profileChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_PROFILE);
119     }
120
121     @Override
122     public void dispose() {
123         super.dispose();
124         if (connectionCheckerFuture != null) {
125             connectionCheckerFuture.cancel(true);
126         }
127         if (statusUpdaterFuture != null) {
128             statusUpdaterFuture.cancel(true);
129         }
130         if (connection != null) {
131             connection.close();
132         }
133     }
134
135     private int getIntConfigParameter(String key, int defaultValue) {
136         Object obj = this.getConfig().get(key);
137         if (obj instanceof Number) {
138             return ((Number) obj).intValue();
139         } else if (obj instanceof String) {
140             return Integer.parseInt(obj.toString());
141         }
142         return defaultValue;
143     }
144
145     @Override
146     public void handleCommand(ChannelUID channelUID, Command command) {
147         switch (channelUID.getIdWithoutGroup()) {
148             case CHANNEL_SCREENSAVER:
149                 if (RefreshType.REFRESH == command) {
150                     connection.updateScreenSaverState();
151                 }
152                 break;
153             case CHANNEL_MUTE:
154                 if (command.equals(OnOffType.ON)) {
155                     connection.setMute(true);
156                 } else if (command.equals(OnOffType.OFF)) {
157                     connection.setMute(false);
158                 } else if (RefreshType.REFRESH == command) {
159                     connection.updateVolume();
160                 }
161                 break;
162             case CHANNEL_VOLUME:
163                 if (command instanceof PercentType) {
164                     connection.setVolume(((PercentType) command).intValue());
165                 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
166                     connection.increaseVolume();
167                 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
168                     connection.decreaseVolume();
169                 } else if (command.equals(OnOffType.OFF)) {
170                     connection.setVolume(0);
171                 } else if (command.equals(OnOffType.ON)) {
172                     connection.setVolume(100);
173                 } else if (RefreshType.REFRESH == command) {
174                     connection.updateVolume();
175                 }
176                 break;
177             case CHANNEL_CONTROL:
178                 if (command instanceof PlayPauseType) {
179                     if (command.equals(PlayPauseType.PLAY)) {
180                         connection.playerPlayPause();
181                     } else if (command.equals(PlayPauseType.PAUSE)) {
182                         connection.playerPlayPause();
183                     }
184                 } else if (command instanceof NextPreviousType) {
185                     if (command.equals(NextPreviousType.NEXT)) {
186                         connection.playerNext();
187                     } else if (command.equals(NextPreviousType.PREVIOUS)) {
188                         connection.playerPrevious();
189                     }
190                 } else if (command instanceof RewindFastforwardType) {
191                     if (command.equals(RewindFastforwardType.REWIND)) {
192                         connection.playerRewind();
193                     } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
194                         connection.playerFastForward();
195                     }
196                 } else if (RefreshType.REFRESH == command) {
197                     connection.updatePlayerStatus();
198                 }
199                 break;
200             case CHANNEL_STOP:
201                 if (command.equals(OnOffType.ON)) {
202                     stop();
203                 } else if (RefreshType.REFRESH == command) {
204                     connection.updatePlayerStatus();
205                 }
206                 break;
207             case CHANNEL_PLAYURI:
208                 if (command instanceof StringType) {
209                     playURI(command);
210                     updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
211                 } else if (RefreshType.REFRESH == command) {
212                     updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
213                 }
214                 break;
215             case CHANNEL_PLAYNOTIFICATION:
216                 if (command instanceof StringType) {
217                     playNotificationSoundURI((StringType) command, true);
218                     updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
219                 } else if (command.equals(RefreshType.REFRESH)) {
220                     updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
221                 }
222                 break;
223             case CHANNEL_PLAYFAVORITE:
224                 if (command instanceof StringType) {
225                     playFavorite(command);
226                     updateState(favoriteChannelUID, UnDefType.UNDEF);
227                 } else if (RefreshType.REFRESH == command) {
228                     updateState(favoriteChannelUID, UnDefType.UNDEF);
229                 }
230                 break;
231             case CHANNEL_PVR_OPEN_TV:
232                 if (command instanceof StringType) {
233                     playPVRChannel(command, PVR_TV, CHANNEL_PVR_OPEN_TV);
234                     updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
235                 } else if (RefreshType.REFRESH == command) {
236                     updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
237                 }
238                 break;
239             case CHANNEL_PVR_OPEN_RADIO:
240                 if (command instanceof StringType) {
241                     playPVRChannel(command, PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
242                     updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
243                 } else if (RefreshType.REFRESH == command) {
244                     updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
245                 }
246                 break;
247             case CHANNEL_SHOWNOTIFICATION:
248                 showNotification(channelUID, command);
249                 break;
250             case CHANNEL_INPUT:
251                 if (command instanceof StringType) {
252                     connection.input(command.toString());
253                     updateState(CHANNEL_INPUT, UnDefType.UNDEF);
254                 } else if (RefreshType.REFRESH == command) {
255                     updateState(CHANNEL_INPUT, UnDefType.UNDEF);
256                 }
257                 break;
258             case CHANNEL_INPUTTEXT:
259                 if (command instanceof StringType) {
260                     connection.inputText(command.toString());
261                     updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
262                 } else if (RefreshType.REFRESH == command) {
263                     updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
264                 }
265                 break;
266             case CHANNEL_INPUTACTION:
267                 if (command instanceof StringType) {
268                     connection.inputAction(command.toString());
269                     updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
270                 } else if (RefreshType.REFRESH == command) {
271                     updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
272                 }
273                 break;
274             case CHANNEL_INPUTBUTTONEVENT:
275                 logger.debug("handleCommand CHANNEL_INPUTBUTTONEVENT {}.", command);
276                 if (command instanceof StringType) {
277                     connection.inputButtonEvent(command.toString());
278                     updateState(CHANNEL_INPUTBUTTONEVENT, UnDefType.UNDEF);
279                 } else if (RefreshType.REFRESH == command) {
280                     updateState(CHANNEL_INPUTBUTTONEVENT, UnDefType.UNDEF);
281                 }
282                 break;
283             case CHANNEL_SYSTEMCOMMAND:
284                 if (command instanceof StringType) {
285                     handleSystemCommand(command.toString());
286                     updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
287                 } else if (RefreshType.REFRESH == command) {
288                     updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
289                 }
290                 break;
291             case CHANNEL_PROFILE:
292                 if (command instanceof StringType) {
293                     connection.profile(command.toString());
294                 } else if (RefreshType.REFRESH == command) {
295                     connection.updateCurrentProfile();
296                 }
297                 break;
298             case CHANNEL_ARTIST:
299             case CHANNEL_ALBUM:
300             case CHANNEL_TITLE:
301             case CHANNEL_SHOWTITLE:
302             case CHANNEL_MEDIATYPE:
303             case CHANNEL_GENRELIST:
304             case CHANNEL_PVR_CHANNEL:
305             case CHANNEL_THUMBNAIL:
306             case CHANNEL_FANART:
307             case CHANNEL_AUDIO_CODEC:
308                 break;
309             case CHANNEL_AUDIO_INDEX:
310                 if (command instanceof DecimalType) {
311                     connection.setAudioStream(((DecimalType) command).intValue());
312                 }
313                 break;
314             case CHANNEL_VIDEO_CODEC:
315             case CHANNEL_VIDEO_INDEX:
316                 if (command instanceof DecimalType) {
317                     connection.setVideoStream(((DecimalType) command).intValue());
318                 }
319                 break;
320             case CHANNEL_SUBTITLE_ENABLED:
321                 if (command.equals(OnOffType.ON)) {
322                     connection.setSubtitleEnabled(true);
323                 } else if (command.equals(OnOffType.OFF)) {
324                     connection.setSubtitleEnabled(false);
325                 }
326                 break;
327             case CHANNEL_SUBTITLE_INDEX:
328                 if (command instanceof DecimalType) {
329                     connection.setSubtitle(((DecimalType) command).intValue());
330                 }
331                 break;
332             case CHANNEL_CURRENTTIME:
333                 if (command instanceof QuantityType) {
334                     connection.setTime(((QuantityType<?>) command).intValue());
335                 }
336                 break;
337             case CHANNEL_CURRENTTIMEPERCENTAGE:
338             case CHANNEL_DURATION:
339                 if (RefreshType.REFRESH == command) {
340                     connection.updatePlayerStatus();
341                 }
342                 break;
343             default:
344                 Channel channel = getThing().getChannel(channelUID);
345                 if (channel != null) {
346                     ChannelTypeUID ctuid = channel.getChannelTypeUID();
347                     if (ctuid != null) {
348                         if (ctuid.getId().equals(CHANNEL_TYPE_SHOWNOTIFICATION)) {
349                             showNotification(channelUID, command);
350                             break;
351                         }
352                     }
353                 }
354                 logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup());
355                 break;
356         }
357     }
358
359     private void showNotification(ChannelUID channelUID, Command command) {
360         if (command instanceof StringType) {
361             Channel channel = getThing().getChannel(channelUID);
362             if (channel != null) {
363                 String title = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_TITLE);
364                 BigDecimal displayTime = (BigDecimal) channel.getConfiguration()
365                         .get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_DISPLAYTIME);
366                 String icon = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_ICON);
367                 connection.showNotification(title, displayTime, icon, command.toString());
368             }
369             updateState(channelUID, UnDefType.UNDEF);
370         } else if (RefreshType.REFRESH == command) {
371             updateState(channelUID, UnDefType.UNDEF);
372         }
373     }
374
375     private URI getImageBaseUrl() throws URISyntaxException {
376         KodiConfig config = getConfigAs(KodiConfig.class);
377         String host = config.getIpAddress();
378         int httpPort = config.getHttpPort();
379         String httpUser = config.getHttpUser();
380         String httpPassword = config.getHttpPassword();
381         String userInfo = httpUser == null || httpUser.isEmpty() || httpPassword == null || httpPassword.isEmpty()
382                 ? null
383                 : String.format("%s:%s", httpUser, httpPassword);
384         return new URI("http", userInfo, host, httpPort, "/image/", null, null);
385     }
386
387     public void stop() {
388         connection.playerStop();
389     }
390
391     public void playURI(Command command) {
392         connection.playURI(command.toString());
393     }
394
395     private void playFavorite(Command command) {
396         KodiFavorite favorite = connection.getFavorite(command.toString());
397         if (favorite != null) {
398             String path = favorite.getPath();
399             String windowParameter = favorite.getWindowParameter();
400             if (path != null && !path.isEmpty()) {
401                 connection.playURI(path);
402             } else if (windowParameter != null && !windowParameter.isEmpty()) {
403                 String[] windowParameters = { windowParameter };
404                 connection.activateWindow(favorite.getWindow(), windowParameters);
405             } else {
406                 connection.activateWindow(favorite.getWindow());
407             }
408         } else {
409             logger.debug("Received unknown favorite '{}'.", command);
410         }
411     }
412
413     public void playPVRChannel(final Command command, final String pvrChannelType, final String channelId) {
414         int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
415         int pvrChannelId = connection.getPVRChannelId(pvrChannelGroupId, command.toString());
416         if (pvrChannelId > 0) {
417             connection.playPVRChannel(pvrChannelId);
418         } else {
419             logger.debug("Received unknown PVR channel '{}'.", command);
420         }
421     }
422
423     private int getPVRChannelGroupId(final String pvrChannelType, final String channelId) {
424         Channel channel = getThing().getChannel(channelId);
425         if (channel != null) {
426             KodiChannelConfig config = channel.getConfiguration().as(KodiChannelConfig.class);
427             String pvrChannelGroupName = config.getGroup();
428             int pvrChannelGroupId = connection.getPVRChannelGroupId(pvrChannelType, pvrChannelGroupName);
429             if (pvrChannelGroupId <= 0) {
430                 logger.debug("Received unknown PVR channel group '{}'. Using default.", pvrChannelGroupName);
431                 pvrChannelGroupId = PVR_TV.equals(pvrChannelType) ? 1 : 2;
432             }
433             return pvrChannelGroupId;
434         }
435         return 0;
436     }
437
438     private void handleSystemCommand(String command) {
439         switch (command) {
440             case SYSTEM_COMMAND_QUIT:
441                 connection.sendApplicationQuit();
442                 break;
443             case SYSTEM_COMMAND_HIBERNATE:
444             case SYSTEM_COMMAND_REBOOT:
445             case SYSTEM_COMMAND_SHUTDOWN:
446             case SYSTEM_COMMAND_SUSPEND:
447                 connection.sendSystemCommand(command);
448                 break;
449             default:
450                 logger.debug("Received unknown system command '{}'.", command);
451                 break;
452         }
453     }
454
455     /*
456      * Play the notification by 1) saving the state of the player, 2) stopping the current
457      * playlist item, 3) adding the notification as a new playlist item, 4) playing the new
458      * playlist item, and 5) restoring the player to its previous state.
459      * set manageVolume to true if the binding must handle volume change by itself
460      */
461     public void playNotificationSoundURI(StringType uri, boolean manageVolume) {
462         // save the current state of the player
463         logger.trace("Saving current player state");
464         KodiPlayerState playerState = new KodiPlayerState();
465         if (manageVolume) {
466             playerState.setSavedVolume(connection.getVolume());
467         }
468         playerState.setPlaylistID(connection.getActivePlaylist());
469         playerState.setSavedState(connection.getState());
470
471         int audioPlaylistID = connection.getPlaylistID("audio");
472         int videoPlaylistID = connection.getPlaylistID("video");
473
474         // pause playback
475         if (KodiState.PLAY.equals(connection.getState())) {
476             // pause if current media is "audio" or "video", stop otherwise
477             if (audioPlaylistID == playerState.getSavedPlaylistID()
478                     || videoPlaylistID == playerState.getSavedPlaylistID()) {
479                 connection.playerPlayPause();
480                 waitForState(KodiState.PAUSE);
481             } else {
482                 connection.playerStop();
483                 waitForState(KodiState.STOP);
484             }
485         }
486
487         // set notification sound volume
488         if (manageVolume) {
489             logger.trace("Setting up player for notification");
490             int notificationVolume = getNotificationSoundVolume().intValue();
491             connection.setVolume(notificationVolume);
492             waitForVolume(notificationVolume);
493         }
494
495         // add the notification uri to the playlist and play it
496         logger.trace("Playing notification");
497         connection.playlistInsert(audioPlaylistID, uri.toString(), 0);
498         waitForPlaylistState(KodiPlaylistState.ADDED);
499
500         connection.playlistPlay(audioPlaylistID, 0);
501         waitForState(KodiState.PLAY);
502         // wait for stop if previous playlist wasn't "audio"
503         if (audioPlaylistID != playerState.getSavedPlaylistID()) {
504             waitForState(KodiState.STOP);
505         }
506
507         // remove the notification uri from the playlist
508         connection.playlistRemove(audioPlaylistID, 0);
509         waitForPlaylistState(KodiPlaylistState.REMOVED);
510
511         // restore previous volume
512         if (manageVolume) {
513             connection.setVolume(playerState.getSavedVolume());
514             waitForVolume(playerState.getSavedVolume());
515         }
516
517         // resume playing save playlist item if player wasn't stopped
518         logger.trace("Restoring player state");
519         switch (playerState.getSavedState()) {
520             case PLAY:
521                 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
522                     connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
523                 }
524                 break;
525             case PAUSE:
526                 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
527                     connection.playerPlayPause();
528                 }
529                 break;
530             case STOP:
531             case END:
532             case FASTFORWARD:
533             case REWIND:
534                 // nothing to do
535                 break;
536         }
537     }
538
539     /*
540      * Wait for the volume status to equal the targetVolume
541      */
542     private boolean waitForVolume(int targetVolume) {
543         int timeoutMaxCount = 20, timeoutCount = 0;
544         logger.trace("Waiting up to {} ms for the volume to be updated ...", timeoutMaxCount * 100);
545         while (targetVolume != connection.getVolume() && timeoutCount < timeoutMaxCount) {
546             try {
547                 Thread.sleep(100);
548             } catch (InterruptedException e) {
549                 break;
550             }
551             timeoutCount++;
552         }
553         return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
554     }
555
556     /*
557      * Wait for the player state so that we know when the notification has started or finished playing
558      */
559     private boolean waitForState(KodiState state) {
560         int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
561         logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 1000, state);
562         while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
563             try {
564                 Thread.sleep(1000);
565             } catch (InterruptedException e) {
566                 break;
567             }
568             timeoutCount++;
569         }
570         return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
571     }
572
573     /*
574      * Wait for the playlist state so that we know when the notification has started or finished playing
575      */
576     private boolean waitForPlaylistState(KodiPlaylistState playlistState) {
577         int timeoutMaxCount = 20, timeoutCount = 0;
578         logger.trace("Waiting up to {} ms for playlist state '{}' to be set ...", timeoutMaxCount * 100, playlistState);
579         while (!playlistState.equals(connection.getPlaylistState()) && timeoutCount < timeoutMaxCount) {
580             try {
581                 Thread.sleep(100);
582             } catch (InterruptedException e) {
583                 break;
584             }
585             timeoutCount++;
586         }
587         return checkForTimeout(timeoutCount, timeoutMaxCount,
588                 "playlist state to '" + playlistState.toString() + "' be set");
589     }
590
591     /*
592      * Log timeout for wait
593      */
594     private boolean checkForTimeout(int timeoutCount, int timeoutLimit, String message) {
595         if (timeoutCount >= timeoutLimit) {
596             logger.debug("TIMEOUT after {} ms waiting for {}!", timeoutCount * 100, message);
597             return false;
598         } else {
599             logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
600             return true;
601         }
602     }
603
604     /**
605      * Gets the current volume level
606      */
607     public PercentType getVolume() {
608         return new PercentType(connection.getVolume());
609     }
610
611     /**
612      * Sets the volume level
613      *
614      * @param volume Volume to be set
615      */
616     public void setVolume(PercentType volume) {
617         if (volume != null) {
618             connection.setVolume(volume.intValue());
619         }
620     }
621
622     /**
623      * Gets the volume level for a notification sound
624      */
625     public PercentType getNotificationSoundVolume() {
626         Integer notificationSoundVolume = getConfigAs(KodiConfig.class).getNotificationVolume();
627         if (notificationSoundVolume == null) {
628             // if no value is set we use the current volume instead
629             return new PercentType(connection.getVolume());
630         }
631         return new PercentType(notificationSoundVolume);
632     }
633
634     /**
635      * Sets the volume level for a notification sound
636      *
637      * @param notificationSoundVolume Volume to be set
638      */
639     public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
640         if (notificationSoundVolume != null) {
641             connection.setVolume(notificationSoundVolume.intValue());
642         }
643     }
644
645     @Override
646     public void initialize() {
647         try {
648             String host = getConfig().get(HOST_PARAMETER).toString();
649             if (host == null || host.isEmpty()) {
650                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
651                         "No network address specified");
652             } else {
653                 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
654
655                 connectionCheckerFuture = scheduler.scheduleWithFixedDelay(() -> {
656                     if (connection.checkConnection()) {
657                         updateFavoriteChannelStateDescription();
658                         updatePVRChannelStateDescription(PVR_TV, CHANNEL_PVR_OPEN_TV);
659                         updatePVRChannelStateDescription(PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
660                         updateProfileStateDescription();
661                     } else {
662                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
663                                 "No connection established");
664                     }
665                 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
666
667                 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
668                     if (KodiState.PLAY.equals(connection.getState())) {
669                         connection.updatePlayerStatus();
670                     }
671                 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
672             }
673         } catch (Exception e) {
674             logger.debug("error during opening connection: {}", e.getMessage(), e);
675             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
676         }
677     }
678
679     private void updateFavoriteChannelStateDescription() {
680         if (isLinked(favoriteChannelUID)) {
681             List<StateOption> options = new ArrayList<>();
682             for (KodiFavorite favorite : connection.getFavorites()) {
683                 options.add(new StateOption(favorite.getTitle(), favorite.getTitle()));
684             }
685             stateDescriptionProvider.setStateOptions(favoriteChannelUID, options);
686         }
687     }
688
689     private void updatePVRChannelStateDescription(final String pvrChannelType, final String channelId) {
690         if (isLinked(channelId)) {
691             int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
692             List<StateOption> options = new ArrayList<>();
693             for (KodiPVRChannel pvrChannel : connection.getPVRChannels(pvrChannelGroupId)) {
694                 options.add(new StateOption(pvrChannel.getLabel(), pvrChannel.getLabel()));
695             }
696             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
697         }
698     }
699
700     private void updateProfileStateDescription() {
701         if (isLinked(profileChannelUID)) {
702             List<StateOption> options = new ArrayList<>();
703             for (KodiProfile profile : connection.getProfiles()) {
704                 options.add(new StateOption(profile.getLabel(), profile.getLabel()));
705             }
706             stateDescriptionProvider.setStateOptions(profileChannelUID, options);
707         }
708     }
709
710     @Override
711     public void updateAudioStreamOptions(List<KodiAudioStream> audios) {
712         if (isLinked(CHANNEL_AUDIO_INDEX)) {
713             List<StateOption> options = new ArrayList<>();
714             for (KodiAudioStream audio : audios) {
715                 options.add(new StateOption(Integer.toString(audio.getIndex()),
716                         audio.getLanguage() + "  [" + audio.getName() + "] (" + audio.getCodec() + "-"
717                                 + Integer.toString(audio.getChannels()) + " "
718                                 + Integer.toString(audio.getBitrate() / 1000) + "kb/s)"));
719             }
720             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
721         }
722     }
723
724     @Override
725     public void updateSubtitleOptions(List<KodiSubtitle> subtitles) {
726         if (isLinked(CHANNEL_SUBTITLE_INDEX)) {
727             List<StateOption> options = new ArrayList<>();
728             for (KodiSubtitle subtitle : subtitles) {
729                 options.add(new StateOption(Integer.toString(subtitle.getIndex()),
730                         subtitle.getLanguage() + "  [" + subtitle.getName() + "]"));
731             }
732             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
733                     options);
734         }
735     }
736
737     @Override
738     public void updateConnectionState(boolean connected) {
739         if (connected) {
740             updateStatus(ThingStatus.ONLINE);
741             scheduler.schedule(() -> connection.getSystemProperties(), 1, TimeUnit.SECONDS);
742             if (isLinked(volumeChannelUID) || isLinked(mutedChannelUID)) {
743                 scheduler.schedule(() -> connection.updateVolume(), 1, TimeUnit.SECONDS);
744             }
745             if (isLinked(profileChannelUID)) {
746                 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
747             }
748             try {
749                 String version = connection.getVersion();
750                 thing.setProperty(PROPERTY_VERSION, version);
751             } catch (Exception e) {
752                 logger.debug("error during reading version: {}", e.getMessage(), e);
753             }
754         } else {
755             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
756         }
757     }
758
759     @Override
760     public void updateScreenSaverState(boolean screenSaverActive) {
761         updateState(screenSaverChannelUID, OnOffType.from(screenSaverActive));
762     }
763
764     @Override
765     public void updateInputRequestedState(boolean inputRequested) {
766         updateState(inputRequestedChannelUID, OnOffType.from(inputRequested));
767     }
768
769     @Override
770     public void updatePlaylistState(KodiPlaylistState playlistState) {
771     }
772
773     @Override
774     public void updateVolume(int volume) {
775         updateState(volumeChannelUID, new PercentType(volume));
776     }
777
778     @Override
779     public void updatePlayerState(KodiState state) {
780         switch (state) {
781             case PLAY:
782                 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
783                 updateState(CHANNEL_STOP, OnOffType.OFF);
784                 break;
785             case PAUSE:
786                 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
787                 updateState(CHANNEL_STOP, OnOffType.OFF);
788                 break;
789             case STOP:
790             case END:
791                 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
792                 updateState(CHANNEL_STOP, OnOffType.ON);
793                 break;
794             case FASTFORWARD:
795                 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
796                 updateState(CHANNEL_STOP, OnOffType.OFF);
797                 break;
798             case REWIND:
799                 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
800                 updateState(CHANNEL_STOP, OnOffType.OFF);
801                 break;
802         }
803     }
804
805     @Override
806     public void updateMuted(boolean muted) {
807         updateState(mutedChannelUID, OnOffType.from(muted));
808     }
809
810     @Override
811     public void updateTitle(String title) {
812         updateState(CHANNEL_TITLE, createStringState(title));
813     }
814
815     @Override
816     public void updateOriginalTitle(String title) {
817         updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
818     }
819
820     @Override
821     public void updateShowTitle(String title) {
822         updateState(CHANNEL_SHOWTITLE, createStringState(title));
823     }
824
825     @Override
826     public void updateAlbum(String album) {
827         updateState(CHANNEL_ALBUM, createStringState(album));
828     }
829
830     @Override
831     public void updateArtistList(List<String> artistList) {
832         updateState(CHANNEL_ARTIST, createStringListState(artistList));
833     }
834
835     @Override
836     public void updateMediaFile(String mediaFile) {
837         updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
838     }
839
840     @Override
841     public void updateMediaType(String mediaType) {
842         updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
843     }
844
845     @Override
846     public void updateMediaID(int mediaid) {
847         updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
848     }
849
850     @Override
851     public void updateRating(double rating) {
852         updateState(CHANNEL_RATING, new DecimalType(rating));
853     }
854
855     @Override
856     public void updateUserRating(double rating) {
857         updateState(CHANNEL_USERRATING, new DecimalType(rating));
858     }
859
860     @Override
861     public void updateMpaa(String mpaa) {
862         updateState(CHANNEL_MPAA, createStringState(mpaa));
863     }
864
865     @Override
866     public void updateUniqueIDDouban(String uniqueid) {
867         updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
868     }
869
870     @Override
871     public void updateUniqueIDImdb(String uniqueid) {
872         updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
873     }
874
875     @Override
876     public void updateUniqueIDTmdb(String uniqueid) {
877         updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
878     }
879
880     @Override
881     public void updateUniqueIDImdbtvshow(String uniqueid) {
882         updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
883     }
884
885     @Override
886     public void updateUniqueIDTmdbtvshow(String uniqueid) {
887         updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
888     }
889
890     @Override
891     public void updateUniqueIDTmdbepisode(String uniqueid) {
892         updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
893     }
894
895     @Override
896     public void updateSeason(int season) {
897         updateState(CHANNEL_SEASON, new DecimalType(season));
898     }
899
900     @Override
901     public void updateEpisode(int episode) {
902         updateState(CHANNEL_EPISODE, new DecimalType(episode));
903     }
904
905     @Override
906     public void updateGenreList(List<String> genreList) {
907         updateState(CHANNEL_GENRELIST, createStringListState(genreList));
908     }
909
910     @Override
911     public void updatePVRChannel(String channel) {
912         updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
913     }
914
915     @Override
916     public void updateThumbnail(RawType thumbnail) {
917         updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
918     }
919
920     @Override
921     public void updateFanart(RawType fanart) {
922         updateState(CHANNEL_FANART, createImageState(fanart));
923     }
924
925     @Override
926     public void updateAudioCodec(String codec) {
927         updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
928     }
929
930     @Override
931     public void updateAudioIndex(int index) {
932         updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
933     }
934
935     @Override
936     public void updateAudioChannels(int channels) {
937         updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
938     }
939
940     @Override
941     public void updateAudioLanguage(String language) {
942         updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
943     }
944
945     @Override
946     public void updateAudioName(String name) {
947         updateState(CHANNEL_AUDIO_NAME, createStringState(name));
948     }
949
950     @Override
951     public void updateVideoCodec(String codec) {
952         updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
953     }
954
955     @Override
956     public void updateVideoIndex(int index) {
957         updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
958     }
959
960     @Override
961     public void updateVideoHeight(int height) {
962         updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
963     }
964
965     @Override
966     public void updateVideoWidth(int width) {
967         updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
968     }
969
970     @Override
971     public void updateSubtitleEnabled(boolean enabled) {
972         updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.from(enabled));
973     }
974
975     @Override
976     public void updateSubtitleIndex(int index) {
977         updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
978     }
979
980     @Override
981     public void updateSubtitleLanguage(String language) {
982         updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
983     }
984
985     @Override
986     public void updateSubtitleName(String name) {
987         updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
988     }
989
990     @Override
991     public void updateCurrentTime(long currentTime) {
992         updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, Units.SECOND));
993     }
994
995     @Override
996     public void updateCurrentTimePercentage(double currentTimePercentage) {
997         updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, Units.PERCENT));
998     }
999
1000     @Override
1001     public void updateDuration(long duration) {
1002         updateState(CHANNEL_DURATION, createQuantityState(duration, Units.SECOND));
1003     }
1004
1005     @Override
1006     public void updateCurrentProfile(String profile) {
1007         updateState(profileChannelUID, new StringType(profile));
1008     }
1009
1010     @Override
1011     public void updateSystemProperties(KodiSystemProperties systemProperties) {
1012         if (systemProperties != null) {
1013             List<CommandOption> options = new ArrayList<>();
1014             if (systemProperties.canHibernate()) {
1015                 options.add(new CommandOption(SYSTEM_COMMAND_HIBERNATE, SYSTEM_COMMAND_HIBERNATE));
1016             }
1017             if (systemProperties.canReboot()) {
1018                 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
1019             }
1020             if (systemProperties.canShutdown()) {
1021                 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
1022             }
1023             if (systemProperties.canSuspend()) {
1024                 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
1025             }
1026             if (systemProperties.canQuit()) {
1027                 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
1028             }
1029             commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1030                     options);
1031         }
1032     }
1033
1034     /**
1035      * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1036      */
1037     private State createStringState(String string) {
1038         if (string == null || string.isEmpty()) {
1039             return UnDefType.UNDEF;
1040         } else {
1041             return new StringType(string);
1042         }
1043     }
1044
1045     /**
1046      * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1047      * Strings is empty.
1048      */
1049     private State createStringListState(List<String> list) {
1050         if (list == null || list.isEmpty()) {
1051             return UnDefType.UNDEF;
1052         } else {
1053             return createStringState(list.stream().collect(Collectors.joining(", ")));
1054         }
1055     }
1056
1057     /**
1058      * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1059      */
1060     private State createImageState(@Nullable RawType image) {
1061         if (image == null) {
1062             return UnDefType.UNDEF;
1063         } else {
1064             return image;
1065         }
1066     }
1067
1068     private State createQuantityState(Number value, Unit<?> unit) {
1069         return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);
1070     }
1071 }