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