2 * Copyright (c) 2010-2021 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_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);
273 case CHANNEL_PROFILE:
274 if (command instanceof StringType) {
275 connection.profile(command.toString());
276 } else if (RefreshType.REFRESH == command) {
277 connection.updateCurrentProfile();
283 case CHANNEL_SHOWTITLE:
284 case CHANNEL_MEDIATYPE:
285 case CHANNEL_GENRELIST:
286 case CHANNEL_PVR_CHANNEL:
287 case CHANNEL_THUMBNAIL:
289 case CHANNEL_AUDIO_CODEC:
291 case CHANNEL_AUDIO_INDEX:
292 if (command instanceof DecimalType) {
293 connection.setAudioStream(((DecimalType) command).intValue());
296 case CHANNEL_VIDEO_CODEC:
297 case CHANNEL_VIDEO_INDEX:
298 if (command instanceof DecimalType) {
299 connection.setVideoStream(((DecimalType) command).intValue());
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);
309 case CHANNEL_SUBTITLE_INDEX:
310 if (command instanceof DecimalType) {
311 connection.setSubtitle(((DecimalType) command).intValue());
314 case CHANNEL_CURRENTTIME:
315 if (command instanceof QuantityType) {
316 connection.setTime(((QuantityType<?>) command).intValue());
319 case CHANNEL_CURRENTTIMEPERCENTAGE:
320 case CHANNEL_DURATION:
321 if (RefreshType.REFRESH == command) {
322 connection.updatePlayerStatus();
326 Channel channel = getThing().getChannel(channelUID);
327 if (channel != null) {
328 ChannelTypeUID ctuid = channel.getChannelTypeUID();
330 if (ctuid.getId().equals(CHANNEL_TYPE_SHOWNOTIFICATION)) {
331 showNotification(channelUID, command);
336 logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup());
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());
351 updateState(channelUID, UnDefType.UNDEF);
352 } else if (RefreshType.REFRESH == command) {
353 updateState(channelUID, UnDefType.UNDEF);
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()
365 : String.format("%s:%s", httpUser, httpPassword);
366 return new URI("http", userInfo, host, httpPort, "/image/", null, null);
370 connection.playerStop();
373 public void playURI(Command command) {
374 connection.playURI(command.toString());
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);
388 connection.activateWindow(favorite.getWindow());
391 logger.debug("Received unknown favorite '{}'.", command);
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);
401 logger.debug("Received unknown PVR channel '{}'.", command);
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;
415 return pvrChannelGroupId;
420 private void handleSystemCommand(String command) {
422 case SYSTEM_COMMAND_QUIT:
423 connection.sendApplicationQuit();
425 case SYSTEM_COMMAND_HIBERNATE:
426 case SYSTEM_COMMAND_REBOOT:
427 case SYSTEM_COMMAND_SHUTDOWN:
428 case SYSTEM_COMMAND_SUSPEND:
429 connection.sendSystemCommand(command);
432 logger.debug("Received unknown system command '{}'.", command);
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.
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());
450 int audioPlaylistID = connection.getPlaylistID("audio");
451 int videoPlaylistID = connection.getPlaylistID("video");
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);
461 connection.playerStop();
462 waitForState(KodiState.STOP);
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);
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);
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);
484 // remove the notification uri from the playlist
485 connection.playlistRemove(audioPlaylistID, 0);
486 waitForPlaylistState(KodiPlaylistState.REMOVED);
488 // restore previous volume
489 connection.setVolume(playerState.getSavedVolume());
490 waitForVolume(playerState.getSavedVolume());
492 // resume playing save playlist item if player wasn't stopped
493 logger.trace("Restoring player state");
494 switch (playerState.getSavedState()) {
496 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
497 connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
501 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
502 connection.playerPlayPause();
515 * Wait for the volume status to equal the targetVolume
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) {
523 } catch (InterruptedException e) {
528 return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
532 * Wait for the player state so that we know when the notification has started or finished playing
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) {
540 } catch (InterruptedException e) {
545 return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
549 * Wait for the playlist state so that we know when the notification has started or finished playing
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) {
557 } catch (InterruptedException e) {
562 return checkForTimeout(timeoutCount, timeoutMaxCount,
563 "playlist state to '" + playlistState.toString() + "' be set");
567 * Log timeout for wait
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);
574 logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
580 * Gets the current volume level
582 public PercentType getVolume() {
583 return new PercentType(connection.getVolume());
587 * Sets the volume level
589 * @param volume Volume to be set
591 public void setVolume(PercentType volume) {
592 if (volume != null) {
593 connection.setVolume(volume.intValue());
598 * Gets the volume level for a notification sound
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());
606 return new PercentType(notificationSoundVolume);
610 * Sets the volume level for a notification sound
612 * @param notificationSoundVolume Volume to be set
614 public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
615 if (notificationSoundVolume != null) {
616 connection.setVolume(notificationSoundVolume.intValue());
621 public void initialize() {
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");
628 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
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();
637 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
638 "No connection established");
640 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
642 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
643 if (KodiState.PLAY.equals(connection.getState())) {
644 connection.updatePlayerStatus();
646 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
648 } catch (Exception e) {
649 logger.debug("error during opening connection: {}", e.getMessage(), e);
650 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
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()));
660 stateDescriptionProvider.setStateOptions(favoriteChannelUID, options);
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()));
671 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
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()));
681 stateDescriptionProvider.setStateOptions(profileChannelUID, options);
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)"));
695 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
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() + "]"));
707 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
713 public void updateConnectionState(boolean 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);
720 if (isLinked(profileChannelUID)) {
721 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
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);
730 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
735 public void updateScreenSaverState(boolean screenSaveActive) {
739 public void updatePlaylistState(KodiPlaylistState playlistState) {
743 public void updateVolume(int volume) {
744 updateState(volumeChannelUID, new PercentType(volume));
748 public void updatePlayerState(KodiState state) {
751 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
752 updateState(CHANNEL_STOP, OnOffType.OFF);
755 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
756 updateState(CHANNEL_STOP, OnOffType.OFF);
760 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
761 updateState(CHANNEL_STOP, OnOffType.ON);
764 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
765 updateState(CHANNEL_STOP, OnOffType.OFF);
768 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
769 updateState(CHANNEL_STOP, OnOffType.OFF);
775 public void updateMuted(boolean muted) {
776 updateState(mutedChannelUID, OnOffType.from(muted));
780 public void updateTitle(String title) {
781 updateState(CHANNEL_TITLE, createStringState(title));
785 public void updateOriginalTitle(String title) {
786 updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
790 public void updateShowTitle(String title) {
791 updateState(CHANNEL_SHOWTITLE, createStringState(title));
795 public void updateAlbum(String album) {
796 updateState(CHANNEL_ALBUM, createStringState(album));
800 public void updateArtistList(List<String> artistList) {
801 updateState(CHANNEL_ARTIST, createStringListState(artistList));
805 public void updateMediaFile(String mediaFile) {
806 updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
810 public void updateMediaType(String mediaType) {
811 updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
815 public void updateMediaID(int mediaid) {
816 updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
820 public void updateRating(double rating) {
821 updateState(CHANNEL_RATING, new DecimalType(rating));
825 public void updateUserRating(double rating) {
826 updateState(CHANNEL_USERRATING, new DecimalType(rating));
830 public void updateMpaa(String mpaa) {
831 updateState(CHANNEL_MPAA, createStringState(mpaa));
835 public void updateUniqueIDDouban(String uniqueid) {
836 updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
840 public void updateUniqueIDImdb(String uniqueid) {
841 updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
845 public void updateUniqueIDTmdb(String uniqueid) {
846 updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
850 public void updateUniqueIDImdbtvshow(String uniqueid) {
851 updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
855 public void updateUniqueIDTmdbtvshow(String uniqueid) {
856 updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
860 public void updateUniqueIDTmdbepisode(String uniqueid) {
861 updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
865 public void updateSeason(int season) {
866 updateState(CHANNEL_SEASON, new DecimalType(season));
870 public void updateEpisode(int episode) {
871 updateState(CHANNEL_EPISODE, new DecimalType(episode));
875 public void updateGenreList(List<String> genreList) {
876 updateState(CHANNEL_GENRELIST, createStringListState(genreList));
880 public void updatePVRChannel(String channel) {
881 updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
885 public void updateThumbnail(RawType thumbnail) {
886 updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
890 public void updateFanart(RawType fanart) {
891 updateState(CHANNEL_FANART, createImageState(fanart));
895 public void updateAudioCodec(String codec) {
896 updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
900 public void updateAudioIndex(int index) {
901 updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
905 public void updateAudioChannels(int channels) {
906 updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
910 public void updateAudioLanguage(String language) {
911 updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
915 public void updateAudioName(String name) {
916 updateState(CHANNEL_AUDIO_NAME, createStringState(name));
920 public void updateVideoCodec(String codec) {
921 updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
925 public void updateVideoIndex(int index) {
926 updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
930 public void updateVideoHeight(int height) {
931 updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
935 public void updateVideoWidth(int width) {
936 updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
940 public void updateSubtitleEnabled(boolean enabled) {
941 updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.from(enabled));
945 public void updateSubtitleIndex(int index) {
946 updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
950 public void updateSubtitleLanguage(String language) {
951 updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
955 public void updateSubtitleName(String name) {
956 updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
960 public void updateCurrentTime(long currentTime) {
961 updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, Units.SECOND));
965 public void updateCurrentTimePercentage(double currentTimePercentage) {
966 updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, Units.PERCENT));
970 public void updateDuration(long duration) {
971 updateState(CHANNEL_DURATION, createQuantityState(duration, Units.SECOND));
975 public void updateCurrentProfile(String profile) {
976 updateState(profileChannelUID, new StringType(profile));
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));
986 if (systemProperties.canReboot()) {
987 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
989 if (systemProperties.canShutdown()) {
990 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
992 if (systemProperties.canSuspend()) {
993 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
995 if (systemProperties.canQuit()) {
996 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
998 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1004 * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1006 private State createStringState(String string) {
1007 if (string == null || string.isEmpty()) {
1008 return UnDefType.UNDEF;
1010 return new StringType(string);
1015 * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1018 private State createStringListState(List<String> list) {
1019 if (list == null || list.isEmpty()) {
1020 return UnDefType.UNDEF;
1022 return createStringState(list.stream().collect(Collectors.joining(", ")));
1027 * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1029 private State createImageState(@Nullable RawType image) {
1030 if (image == null) {
1031 return UnDefType.UNDEF;
1037 private State createQuantityState(Number value, Unit<?> unit) {
1038 return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);