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