2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.kodi.internal.handler;
15 import static org.openhab.binding.kodi.internal.KodiBindingConstants.*;
17 import java.math.BigDecimal;
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;
26 import javax.measure.Unit;
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;
71 * The {@link KodiHandler} is responsible for handling commands, which are sent
72 * to one of the channels.
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
80 public class KodiHandler extends BaseThingHandler implements KodiEventListener {
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";
88 private final Logger logger = LoggerFactory.getLogger(KodiHandler.class);
90 private final KodiConnection connection;
91 private final KodiDynamicCommandDescriptionProvider commandDescriptionProvider;
92 private final KodiDynamicStateDescriptionProvider stateDescriptionProvider;
94 private final ChannelUID volumeChannelUID;
95 private final ChannelUID mutedChannelUID;
96 private final ChannelUID favoriteChannelUID;
97 private final ChannelUID profileChannelUID;
99 private ScheduledFuture<?> connectionCheckerFuture;
100 private ScheduledFuture<?> statusUpdaterFuture;
102 public KodiHandler(Thing thing, KodiDynamicCommandDescriptionProvider commandDescriptionProvider,
103 KodiDynamicStateDescriptionProvider stateDescriptionProvider, WebSocketClient webSocketClient,
104 String callbackUrl) {
106 connection = new KodiConnection(this, webSocketClient, callbackUrl);
108 this.commandDescriptionProvider = commandDescriptionProvider;
109 this.stateDescriptionProvider = stateDescriptionProvider;
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);
118 public void dispose() {
120 if (connectionCheckerFuture != null) {
121 connectionCheckerFuture.cancel(true);
123 if (statusUpdaterFuture != null) {
124 statusUpdaterFuture.cancel(true);
126 if (connection != null) {
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());
142 public void handleCommand(ChannelUID channelUID, Command command) {
143 switch (channelUID.getIdWithoutGroup()) {
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();
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();
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();
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();
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();
187 } else if (RefreshType.REFRESH == command) {
188 connection.updatePlayerStatus();
192 if (command.equals(OnOffType.ON)) {
194 } else if (RefreshType.REFRESH == command) {
195 connection.updatePlayerStatus();
198 case CHANNEL_PLAYURI:
199 if (command instanceof StringType) {
201 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
202 } else if (RefreshType.REFRESH == command) {
203 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
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);
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);
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);
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);
238 case CHANNEL_SHOWNOTIFICATION:
239 showNotification(channelUID, command);
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);
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);
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);
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);
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);
282 case CHANNEL_PROFILE:
283 if (command instanceof StringType) {
284 connection.profile(command.toString());
285 } else if (RefreshType.REFRESH == command) {
286 connection.updateCurrentProfile();
292 case CHANNEL_SHOWTITLE:
293 case CHANNEL_MEDIATYPE:
294 case CHANNEL_GENRELIST:
295 case CHANNEL_PVR_CHANNEL:
296 case CHANNEL_THUMBNAIL:
298 case CHANNEL_AUDIO_CODEC:
300 case CHANNEL_AUDIO_INDEX:
301 if (command instanceof DecimalType) {
302 connection.setAudioStream(((DecimalType) command).intValue());
305 case CHANNEL_VIDEO_CODEC:
306 case CHANNEL_VIDEO_INDEX:
307 if (command instanceof DecimalType) {
308 connection.setVideoStream(((DecimalType) command).intValue());
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);
318 case CHANNEL_SUBTITLE_INDEX:
319 if (command instanceof DecimalType) {
320 connection.setSubtitle(((DecimalType) command).intValue());
323 case CHANNEL_CURRENTTIME:
324 if (command instanceof QuantityType) {
325 connection.setTime(((QuantityType<?>) command).intValue());
328 case CHANNEL_CURRENTTIMEPERCENTAGE:
329 case CHANNEL_DURATION:
330 if (RefreshType.REFRESH == command) {
331 connection.updatePlayerStatus();
335 Channel channel = getThing().getChannel(channelUID);
336 if (channel != null) {
337 ChannelTypeUID ctuid = channel.getChannelTypeUID();
339 if (ctuid.getId().equals(CHANNEL_TYPE_SHOWNOTIFICATION)) {
340 showNotification(channelUID, command);
345 logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup());
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());
360 updateState(channelUID, UnDefType.UNDEF);
361 } else if (RefreshType.REFRESH == command) {
362 updateState(channelUID, UnDefType.UNDEF);
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()
374 : String.format("%s:%s", httpUser, httpPassword);
375 return new URI("http", userInfo, host, httpPort, "/image/", null, null);
379 connection.playerStop();
382 public void playURI(Command command) {
383 connection.playURI(command.toString());
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);
397 connection.activateWindow(favorite.getWindow());
400 logger.debug("Received unknown favorite '{}'.", command);
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);
410 logger.debug("Received unknown PVR channel '{}'.", command);
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;
424 return pvrChannelGroupId;
429 private void handleSystemCommand(String command) {
431 case SYSTEM_COMMAND_QUIT:
432 connection.sendApplicationQuit();
434 case SYSTEM_COMMAND_HIBERNATE:
435 case SYSTEM_COMMAND_REBOOT:
436 case SYSTEM_COMMAND_SHUTDOWN:
437 case SYSTEM_COMMAND_SUSPEND:
438 connection.sendSystemCommand(command);
441 logger.debug("Received unknown system command '{}'.", command);
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.
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());
459 int audioPlaylistID = connection.getPlaylistID("audio");
460 int videoPlaylistID = connection.getPlaylistID("video");
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);
470 connection.playerStop();
471 waitForState(KodiState.STOP);
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);
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);
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);
493 // remove the notification uri from the playlist
494 connection.playlistRemove(audioPlaylistID, 0);
495 waitForPlaylistState(KodiPlaylistState.REMOVED);
497 // restore previous volume
498 connection.setVolume(playerState.getSavedVolume());
499 waitForVolume(playerState.getSavedVolume());
501 // resume playing save playlist item if player wasn't stopped
502 logger.trace("Restoring player state");
503 switch (playerState.getSavedState()) {
505 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
506 connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
510 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
511 connection.playerPlayPause();
524 * Wait for the volume status to equal the targetVolume
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) {
532 } catch (InterruptedException e) {
537 return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
541 * Wait for the player state so that we know when the notification has started or finished playing
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) {
549 } catch (InterruptedException e) {
554 return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
558 * Wait for the playlist state so that we know when the notification has started or finished playing
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) {
566 } catch (InterruptedException e) {
571 return checkForTimeout(timeoutCount, timeoutMaxCount,
572 "playlist state to '" + playlistState.toString() + "' be set");
576 * Log timeout for wait
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);
583 logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
589 * Gets the current volume level
591 public PercentType getVolume() {
592 return new PercentType(connection.getVolume());
596 * Sets the volume level
598 * @param volume Volume to be set
600 public void setVolume(PercentType volume) {
601 if (volume != null) {
602 connection.setVolume(volume.intValue());
607 * Gets the volume level for a notification sound
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());
615 return new PercentType(notificationSoundVolume);
619 * Sets the volume level for a notification sound
621 * @param notificationSoundVolume Volume to be set
623 public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
624 if (notificationSoundVolume != null) {
625 connection.setVolume(notificationSoundVolume.intValue());
630 public void initialize() {
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");
637 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
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();
646 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
647 "No connection established");
649 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
651 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
652 if (KodiState.PLAY.equals(connection.getState())) {
653 connection.updatePlayerStatus();
655 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
657 } catch (Exception e) {
658 logger.debug("error during opening connection: {}", e.getMessage(), e);
659 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
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()));
669 stateDescriptionProvider.setStateOptions(favoriteChannelUID, options);
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()));
680 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
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()));
690 stateDescriptionProvider.setStateOptions(profileChannelUID, options);
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)"));
704 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
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() + "]"));
716 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
722 public void updateConnectionState(boolean 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);
729 if (isLinked(profileChannelUID)) {
730 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
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);
739 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
744 public void updateScreenSaverState(boolean screenSaveActive) {
748 public void updatePlaylistState(KodiPlaylistState playlistState) {
752 public void updateVolume(int volume) {
753 updateState(volumeChannelUID, new PercentType(volume));
757 public void updatePlayerState(KodiState state) {
760 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
761 updateState(CHANNEL_STOP, OnOffType.OFF);
764 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
765 updateState(CHANNEL_STOP, OnOffType.OFF);
769 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
770 updateState(CHANNEL_STOP, OnOffType.ON);
773 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
774 updateState(CHANNEL_STOP, OnOffType.OFF);
777 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
778 updateState(CHANNEL_STOP, OnOffType.OFF);
784 public void updateMuted(boolean muted) {
785 updateState(mutedChannelUID, OnOffType.from(muted));
789 public void updateTitle(String title) {
790 updateState(CHANNEL_TITLE, createStringState(title));
794 public void updateOriginalTitle(String title) {
795 updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
799 public void updateShowTitle(String title) {
800 updateState(CHANNEL_SHOWTITLE, createStringState(title));
804 public void updateAlbum(String album) {
805 updateState(CHANNEL_ALBUM, createStringState(album));
809 public void updateArtistList(List<String> artistList) {
810 updateState(CHANNEL_ARTIST, createStringListState(artistList));
814 public void updateMediaFile(String mediaFile) {
815 updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
819 public void updateMediaType(String mediaType) {
820 updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
824 public void updateMediaID(int mediaid) {
825 updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
829 public void updateRating(double rating) {
830 updateState(CHANNEL_RATING, new DecimalType(rating));
834 public void updateUserRating(double rating) {
835 updateState(CHANNEL_USERRATING, new DecimalType(rating));
839 public void updateMpaa(String mpaa) {
840 updateState(CHANNEL_MPAA, createStringState(mpaa));
844 public void updateUniqueIDDouban(String uniqueid) {
845 updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
849 public void updateUniqueIDImdb(String uniqueid) {
850 updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
854 public void updateUniqueIDTmdb(String uniqueid) {
855 updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
859 public void updateUniqueIDImdbtvshow(String uniqueid) {
860 updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
864 public void updateUniqueIDTmdbtvshow(String uniqueid) {
865 updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
869 public void updateUniqueIDTmdbepisode(String uniqueid) {
870 updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
874 public void updateSeason(int season) {
875 updateState(CHANNEL_SEASON, new DecimalType(season));
879 public void updateEpisode(int episode) {
880 updateState(CHANNEL_EPISODE, new DecimalType(episode));
884 public void updateGenreList(List<String> genreList) {
885 updateState(CHANNEL_GENRELIST, createStringListState(genreList));
889 public void updatePVRChannel(String channel) {
890 updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
894 public void updateThumbnail(RawType thumbnail) {
895 updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
899 public void updateFanart(RawType fanart) {
900 updateState(CHANNEL_FANART, createImageState(fanart));
904 public void updateAudioCodec(String codec) {
905 updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
909 public void updateAudioIndex(int index) {
910 updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
914 public void updateAudioChannels(int channels) {
915 updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
919 public void updateAudioLanguage(String language) {
920 updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
924 public void updateAudioName(String name) {
925 updateState(CHANNEL_AUDIO_NAME, createStringState(name));
929 public void updateVideoCodec(String codec) {
930 updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
934 public void updateVideoIndex(int index) {
935 updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
939 public void updateVideoHeight(int height) {
940 updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
944 public void updateVideoWidth(int width) {
945 updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
949 public void updateSubtitleEnabled(boolean enabled) {
950 updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.from(enabled));
954 public void updateSubtitleIndex(int index) {
955 updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
959 public void updateSubtitleLanguage(String language) {
960 updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
964 public void updateSubtitleName(String name) {
965 updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
969 public void updateCurrentTime(long currentTime) {
970 updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, Units.SECOND));
974 public void updateCurrentTimePercentage(double currentTimePercentage) {
975 updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, Units.PERCENT));
979 public void updateDuration(long duration) {
980 updateState(CHANNEL_DURATION, createQuantityState(duration, Units.SECOND));
984 public void updateCurrentProfile(String profile) {
985 updateState(profileChannelUID, new StringType(profile));
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));
995 if (systemProperties.canReboot()) {
996 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
998 if (systemProperties.canShutdown()) {
999 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
1001 if (systemProperties.canSuspend()) {
1002 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
1004 if (systemProperties.canQuit()) {
1005 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
1007 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1013 * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1015 private State createStringState(String string) {
1016 if (string == null || string.isEmpty()) {
1017 return UnDefType.UNDEF;
1019 return new StringType(string);
1024 * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1027 private State createStringListState(List<String> list) {
1028 if (list == null || list.isEmpty()) {
1029 return UnDefType.UNDEF;
1031 return createStringState(list.stream().collect(Collectors.joining(", ")));
1036 * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1038 private State createImageState(@Nullable RawType image) {
1039 if (image == null) {
1040 return UnDefType.UNDEF;
1046 private State createQuantityState(Number value, Unit<?> unit) {
1047 return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);