]> git.basschouten.com Git - openhab-addons.git/blob
d27e4a37ab2cc6822510c12a201f5cf0e09df294
[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);
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      */
460     public void playNotificationSoundURI(StringType uri) {
461         // save the current state of the player
462         logger.trace("Saving current player state");
463         KodiPlayerState playerState = new KodiPlayerState();
464         playerState.setSavedVolume(connection.getVolume());
465         playerState.setPlaylistID(connection.getActivePlaylist());
466         playerState.setSavedState(connection.getState());
467
468         int audioPlaylistID = connection.getPlaylistID("audio");
469         int videoPlaylistID = connection.getPlaylistID("video");
470
471         // pause playback
472         if (KodiState.PLAY.equals(connection.getState())) {
473             // pause if current media is "audio" or "video", stop otherwise
474             if (audioPlaylistID == playerState.getSavedPlaylistID()
475                     || videoPlaylistID == playerState.getSavedPlaylistID()) {
476                 connection.playerPlayPause();
477                 waitForState(KodiState.PAUSE);
478             } else {
479                 connection.playerStop();
480                 waitForState(KodiState.STOP);
481             }
482         }
483
484         // set notification sound volume
485         logger.trace("Setting up player for notification");
486         int notificationVolume = getNotificationSoundVolume().intValue();
487         connection.setVolume(notificationVolume);
488         waitForVolume(notificationVolume);
489
490         // add the notification uri to the playlist and play it
491         logger.trace("Playing notification");
492         connection.playlistInsert(audioPlaylistID, uri.toString(), 0);
493         waitForPlaylistState(KodiPlaylistState.ADDED);
494
495         connection.playlistPlay(audioPlaylistID, 0);
496         waitForState(KodiState.PLAY);
497         // wait for stop if previous playlist wasn't "audio"
498         if (audioPlaylistID != playerState.getSavedPlaylistID()) {
499             waitForState(KodiState.STOP);
500         }
501
502         // remove the notification uri from the playlist
503         connection.playlistRemove(audioPlaylistID, 0);
504         waitForPlaylistState(KodiPlaylistState.REMOVED);
505
506         // restore previous volume
507         connection.setVolume(playerState.getSavedVolume());
508         waitForVolume(playerState.getSavedVolume());
509
510         // resume playing save playlist item if player wasn't stopped
511         logger.trace("Restoring player state");
512         switch (playerState.getSavedState()) {
513             case PLAY:
514                 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
515                     connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
516                 }
517                 break;
518             case PAUSE:
519                 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
520                     connection.playerPlayPause();
521                 }
522                 break;
523             case STOP:
524             case END:
525             case FASTFORWARD:
526             case REWIND:
527                 // nothing to do
528                 break;
529         }
530     }
531
532     /*
533      * Wait for the volume status to equal the targetVolume
534      */
535     private boolean waitForVolume(int targetVolume) {
536         int timeoutMaxCount = 20, timeoutCount = 0;
537         logger.trace("Waiting up to {} ms for the volume to be updated ...", timeoutMaxCount * 100);
538         while (targetVolume != connection.getVolume() && timeoutCount < timeoutMaxCount) {
539             try {
540                 Thread.sleep(100);
541             } catch (InterruptedException e) {
542                 break;
543             }
544             timeoutCount++;
545         }
546         return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
547     }
548
549     /*
550      * Wait for the player state so that we know when the notification has started or finished playing
551      */
552     private boolean waitForState(KodiState state) {
553         int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
554         logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 100, state);
555         while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
556             try {
557                 Thread.sleep(100);
558             } catch (InterruptedException e) {
559                 break;
560             }
561             timeoutCount++;
562         }
563         return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
564     }
565
566     /*
567      * Wait for the playlist state so that we know when the notification has started or finished playing
568      */
569     private boolean waitForPlaylistState(KodiPlaylistState playlistState) {
570         int timeoutMaxCount = 20, timeoutCount = 0;
571         logger.trace("Waiting up to {} ms for playlist state '{}' to be set ...", timeoutMaxCount * 100, playlistState);
572         while (!playlistState.equals(connection.getPlaylistState()) && timeoutCount < timeoutMaxCount) {
573             try {
574                 Thread.sleep(100);
575             } catch (InterruptedException e) {
576                 break;
577             }
578             timeoutCount++;
579         }
580         return checkForTimeout(timeoutCount, timeoutMaxCount,
581                 "playlist state to '" + playlistState.toString() + "' be set");
582     }
583
584     /*
585      * Log timeout for wait
586      */
587     private boolean checkForTimeout(int timeoutCount, int timeoutLimit, String message) {
588         if (timeoutCount >= timeoutLimit) {
589             logger.debug("TIMEOUT after {} ms waiting for {}!", timeoutCount * 100, message);
590             return false;
591         } else {
592             logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
593             return true;
594         }
595     }
596
597     /**
598      * Gets the current volume level
599      */
600     public PercentType getVolume() {
601         return new PercentType(connection.getVolume());
602     }
603
604     /**
605      * Sets the volume level
606      *
607      * @param volume Volume to be set
608      */
609     public void setVolume(PercentType volume) {
610         if (volume != null) {
611             connection.setVolume(volume.intValue());
612         }
613     }
614
615     /**
616      * Gets the volume level for a notification sound
617      */
618     public PercentType getNotificationSoundVolume() {
619         Integer notificationSoundVolume = getConfigAs(KodiConfig.class).getNotificationVolume();
620         if (notificationSoundVolume == null) {
621             // if no value is set we use the current volume instead
622             return new PercentType(connection.getVolume());
623         }
624         return new PercentType(notificationSoundVolume);
625     }
626
627     /**
628      * Sets the volume level for a notification sound
629      *
630      * @param notificationSoundVolume Volume to be set
631      */
632     public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
633         if (notificationSoundVolume != null) {
634             connection.setVolume(notificationSoundVolume.intValue());
635         }
636     }
637
638     @Override
639     public void initialize() {
640         try {
641             String host = getConfig().get(HOST_PARAMETER).toString();
642             if (host == null || host.isEmpty()) {
643                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
644                         "No network address specified");
645             } else {
646                 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
647
648                 connectionCheckerFuture = scheduler.scheduleWithFixedDelay(() -> {
649                     if (connection.checkConnection()) {
650                         updateFavoriteChannelStateDescription();
651                         updatePVRChannelStateDescription(PVR_TV, CHANNEL_PVR_OPEN_TV);
652                         updatePVRChannelStateDescription(PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
653                         updateProfileStateDescription();
654                     } else {
655                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
656                                 "No connection established");
657                     }
658                 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
659
660                 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
661                     if (KodiState.PLAY.equals(connection.getState())) {
662                         connection.updatePlayerStatus();
663                     }
664                 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
665             }
666         } catch (Exception e) {
667             logger.debug("error during opening connection: {}", e.getMessage(), e);
668             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
669         }
670     }
671
672     private void updateFavoriteChannelStateDescription() {
673         if (isLinked(favoriteChannelUID)) {
674             List<StateOption> options = new ArrayList<>();
675             for (KodiFavorite favorite : connection.getFavorites()) {
676                 options.add(new StateOption(favorite.getTitle(), favorite.getTitle()));
677             }
678             stateDescriptionProvider.setStateOptions(favoriteChannelUID, options);
679         }
680     }
681
682     private void updatePVRChannelStateDescription(final String pvrChannelType, final String channelId) {
683         if (isLinked(channelId)) {
684             int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
685             List<StateOption> options = new ArrayList<>();
686             for (KodiPVRChannel pvrChannel : connection.getPVRChannels(pvrChannelGroupId)) {
687                 options.add(new StateOption(pvrChannel.getLabel(), pvrChannel.getLabel()));
688             }
689             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
690         }
691     }
692
693     private void updateProfileStateDescription() {
694         if (isLinked(profileChannelUID)) {
695             List<StateOption> options = new ArrayList<>();
696             for (KodiProfile profile : connection.getProfiles()) {
697                 options.add(new StateOption(profile.getLabel(), profile.getLabel()));
698             }
699             stateDescriptionProvider.setStateOptions(profileChannelUID, options);
700         }
701     }
702
703     @Override
704     public void updateAudioStreamOptions(List<KodiAudioStream> audios) {
705         if (isLinked(CHANNEL_AUDIO_INDEX)) {
706             List<StateOption> options = new ArrayList<>();
707             for (KodiAudioStream audio : audios) {
708                 options.add(new StateOption(Integer.toString(audio.getIndex()),
709                         audio.getLanguage() + "  [" + audio.getName() + "] (" + audio.getCodec() + "-"
710                                 + Integer.toString(audio.getChannels()) + " "
711                                 + Integer.toString(audio.getBitrate() / 1000) + "kb/s)"));
712             }
713             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
714         }
715     }
716
717     @Override
718     public void updateSubtitleOptions(List<KodiSubtitle> subtitles) {
719         if (isLinked(CHANNEL_SUBTITLE_INDEX)) {
720             List<StateOption> options = new ArrayList<>();
721             for (KodiSubtitle subtitle : subtitles) {
722                 options.add(new StateOption(Integer.toString(subtitle.getIndex()),
723                         subtitle.getLanguage() + "  [" + subtitle.getName() + "]"));
724             }
725             stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
726                     options);
727         }
728     }
729
730     @Override
731     public void updateConnectionState(boolean connected) {
732         if (connected) {
733             updateStatus(ThingStatus.ONLINE);
734             scheduler.schedule(() -> connection.getSystemProperties(), 1, TimeUnit.SECONDS);
735             if (isLinked(volumeChannelUID) || isLinked(mutedChannelUID)) {
736                 scheduler.schedule(() -> connection.updateVolume(), 1, TimeUnit.SECONDS);
737             }
738             if (isLinked(profileChannelUID)) {
739                 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
740             }
741             try {
742                 String version = connection.getVersion();
743                 thing.setProperty(PROPERTY_VERSION, version);
744             } catch (Exception e) {
745                 logger.debug("error during reading version: {}", e.getMessage(), e);
746             }
747         } else {
748             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
749         }
750     }
751
752     @Override
753     public void updateScreenSaverState(boolean screenSaverActive) {
754         updateState(screenSaverChannelUID, OnOffType.from(screenSaverActive));
755     }
756
757     @Override
758     public void updateInputRequestedState(boolean inputRequested) {
759         updateState(inputRequestedChannelUID, OnOffType.from(inputRequested));
760     }
761
762     @Override
763     public void updatePlaylistState(KodiPlaylistState playlistState) {
764     }
765
766     @Override
767     public void updateVolume(int volume) {
768         updateState(volumeChannelUID, new PercentType(volume));
769     }
770
771     @Override
772     public void updatePlayerState(KodiState state) {
773         switch (state) {
774             case PLAY:
775                 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
776                 updateState(CHANNEL_STOP, OnOffType.OFF);
777                 break;
778             case PAUSE:
779                 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
780                 updateState(CHANNEL_STOP, OnOffType.OFF);
781                 break;
782             case STOP:
783             case END:
784                 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
785                 updateState(CHANNEL_STOP, OnOffType.ON);
786                 break;
787             case FASTFORWARD:
788                 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
789                 updateState(CHANNEL_STOP, OnOffType.OFF);
790                 break;
791             case REWIND:
792                 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
793                 updateState(CHANNEL_STOP, OnOffType.OFF);
794                 break;
795         }
796     }
797
798     @Override
799     public void updateMuted(boolean muted) {
800         updateState(mutedChannelUID, OnOffType.from(muted));
801     }
802
803     @Override
804     public void updateTitle(String title) {
805         updateState(CHANNEL_TITLE, createStringState(title));
806     }
807
808     @Override
809     public void updateOriginalTitle(String title) {
810         updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
811     }
812
813     @Override
814     public void updateShowTitle(String title) {
815         updateState(CHANNEL_SHOWTITLE, createStringState(title));
816     }
817
818     @Override
819     public void updateAlbum(String album) {
820         updateState(CHANNEL_ALBUM, createStringState(album));
821     }
822
823     @Override
824     public void updateArtistList(List<String> artistList) {
825         updateState(CHANNEL_ARTIST, createStringListState(artistList));
826     }
827
828     @Override
829     public void updateMediaFile(String mediaFile) {
830         updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
831     }
832
833     @Override
834     public void updateMediaType(String mediaType) {
835         updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
836     }
837
838     @Override
839     public void updateMediaID(int mediaid) {
840         updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
841     }
842
843     @Override
844     public void updateRating(double rating) {
845         updateState(CHANNEL_RATING, new DecimalType(rating));
846     }
847
848     @Override
849     public void updateUserRating(double rating) {
850         updateState(CHANNEL_USERRATING, new DecimalType(rating));
851     }
852
853     @Override
854     public void updateMpaa(String mpaa) {
855         updateState(CHANNEL_MPAA, createStringState(mpaa));
856     }
857
858     @Override
859     public void updateUniqueIDDouban(String uniqueid) {
860         updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
861     }
862
863     @Override
864     public void updateUniqueIDImdb(String uniqueid) {
865         updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
866     }
867
868     @Override
869     public void updateUniqueIDTmdb(String uniqueid) {
870         updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
871     }
872
873     @Override
874     public void updateUniqueIDImdbtvshow(String uniqueid) {
875         updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
876     }
877
878     @Override
879     public void updateUniqueIDTmdbtvshow(String uniqueid) {
880         updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
881     }
882
883     @Override
884     public void updateUniqueIDTmdbepisode(String uniqueid) {
885         updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
886     }
887
888     @Override
889     public void updateSeason(int season) {
890         updateState(CHANNEL_SEASON, new DecimalType(season));
891     }
892
893     @Override
894     public void updateEpisode(int episode) {
895         updateState(CHANNEL_EPISODE, new DecimalType(episode));
896     }
897
898     @Override
899     public void updateGenreList(List<String> genreList) {
900         updateState(CHANNEL_GENRELIST, createStringListState(genreList));
901     }
902
903     @Override
904     public void updatePVRChannel(String channel) {
905         updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
906     }
907
908     @Override
909     public void updateThumbnail(RawType thumbnail) {
910         updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
911     }
912
913     @Override
914     public void updateFanart(RawType fanart) {
915         updateState(CHANNEL_FANART, createImageState(fanart));
916     }
917
918     @Override
919     public void updateAudioCodec(String codec) {
920         updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
921     }
922
923     @Override
924     public void updateAudioIndex(int index) {
925         updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
926     }
927
928     @Override
929     public void updateAudioChannels(int channels) {
930         updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
931     }
932
933     @Override
934     public void updateAudioLanguage(String language) {
935         updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
936     }
937
938     @Override
939     public void updateAudioName(String name) {
940         updateState(CHANNEL_AUDIO_NAME, createStringState(name));
941     }
942
943     @Override
944     public void updateVideoCodec(String codec) {
945         updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
946     }
947
948     @Override
949     public void updateVideoIndex(int index) {
950         updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
951     }
952
953     @Override
954     public void updateVideoHeight(int height) {
955         updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
956     }
957
958     @Override
959     public void updateVideoWidth(int width) {
960         updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
961     }
962
963     @Override
964     public void updateSubtitleEnabled(boolean enabled) {
965         updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.from(enabled));
966     }
967
968     @Override
969     public void updateSubtitleIndex(int index) {
970         updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
971     }
972
973     @Override
974     public void updateSubtitleLanguage(String language) {
975         updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
976     }
977
978     @Override
979     public void updateSubtitleName(String name) {
980         updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
981     }
982
983     @Override
984     public void updateCurrentTime(long currentTime) {
985         updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, Units.SECOND));
986     }
987
988     @Override
989     public void updateCurrentTimePercentage(double currentTimePercentage) {
990         updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, Units.PERCENT));
991     }
992
993     @Override
994     public void updateDuration(long duration) {
995         updateState(CHANNEL_DURATION, createQuantityState(duration, Units.SECOND));
996     }
997
998     @Override
999     public void updateCurrentProfile(String profile) {
1000         updateState(profileChannelUID, new StringType(profile));
1001     }
1002
1003     @Override
1004     public void updateSystemProperties(KodiSystemProperties systemProperties) {
1005         if (systemProperties != null) {
1006             List<CommandOption> options = new ArrayList<>();
1007             if (systemProperties.canHibernate()) {
1008                 options.add(new CommandOption(SYSTEM_COMMAND_HIBERNATE, SYSTEM_COMMAND_HIBERNATE));
1009             }
1010             if (systemProperties.canReboot()) {
1011                 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
1012             }
1013             if (systemProperties.canShutdown()) {
1014                 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
1015             }
1016             if (systemProperties.canSuspend()) {
1017                 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
1018             }
1019             if (systemProperties.canQuit()) {
1020                 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
1021             }
1022             commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1023                     options);
1024         }
1025     }
1026
1027     /**
1028      * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1029      */
1030     private State createStringState(String string) {
1031         if (string == null || string.isEmpty()) {
1032             return UnDefType.UNDEF;
1033         } else {
1034             return new StringType(string);
1035         }
1036     }
1037
1038     /**
1039      * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1040      * Strings is empty.
1041      */
1042     private State createStringListState(List<String> list) {
1043         if (list == null || list.isEmpty()) {
1044             return UnDefType.UNDEF;
1045         } else {
1046             return createStringState(list.stream().collect(Collectors.joining(", ")));
1047         }
1048     }
1049
1050     /**
1051      * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1052      */
1053     private State createImageState(@Nullable RawType image) {
1054         if (image == null) {
1055             return UnDefType.UNDEF;
1056         } else {
1057             return image;
1058         }
1059     }
1060
1061     private State createQuantityState(Number value, Unit<?> unit) {
1062         return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);
1063     }
1064 }