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