2 * Copyright (c) 2010-2023 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 screenSaverChannelUID;
95 private final ChannelUID inputRequestedChannelUID;
96 private final ChannelUID volumeChannelUID;
97 private final ChannelUID mutedChannelUID;
98 private final ChannelUID favoriteChannelUID;
99 private final ChannelUID profileChannelUID;
101 private ScheduledFuture<?> connectionCheckerFuture;
102 private ScheduledFuture<?> statusUpdaterFuture;
104 public KodiHandler(Thing thing, KodiDynamicCommandDescriptionProvider commandDescriptionProvider,
105 KodiDynamicStateDescriptionProvider stateDescriptionProvider, WebSocketClient webSocketClient,
106 String callbackUrl) {
108 connection = new KodiConnection(this, webSocketClient, callbackUrl);
110 this.commandDescriptionProvider = commandDescriptionProvider;
111 this.stateDescriptionProvider = stateDescriptionProvider;
113 screenSaverChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_SCREENSAVER);
114 inputRequestedChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_INPUTREQUESTED);
115 volumeChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_VOLUME);
116 mutedChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_MUTE);
117 favoriteChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_PLAYFAVORITE);
118 profileChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_PROFILE);
122 public void dispose() {
124 if (connectionCheckerFuture != null) {
125 connectionCheckerFuture.cancel(true);
127 if (statusUpdaterFuture != null) {
128 statusUpdaterFuture.cancel(true);
130 if (connection != null) {
135 private int getIntConfigParameter(String key, int defaultValue) {
136 Object obj = this.getConfig().get(key);
137 if (obj instanceof Number) {
138 return ((Number) obj).intValue();
139 } else if (obj instanceof String) {
140 return Integer.parseInt(obj.toString());
146 public void handleCommand(ChannelUID channelUID, Command command) {
147 switch (channelUID.getIdWithoutGroup()) {
148 case CHANNEL_SCREENSAVER:
149 if (RefreshType.REFRESH == command) {
150 connection.updateScreenSaverState();
154 if (command.equals(OnOffType.ON)) {
155 connection.setMute(true);
156 } else if (command.equals(OnOffType.OFF)) {
157 connection.setMute(false);
158 } else if (RefreshType.REFRESH == command) {
159 connection.updateVolume();
163 if (command instanceof PercentType) {
164 connection.setVolume(((PercentType) command).intValue());
165 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
166 connection.increaseVolume();
167 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
168 connection.decreaseVolume();
169 } else if (command.equals(OnOffType.OFF)) {
170 connection.setVolume(0);
171 } else if (command.equals(OnOffType.ON)) {
172 connection.setVolume(100);
173 } else if (RefreshType.REFRESH == command) {
174 connection.updateVolume();
177 case CHANNEL_CONTROL:
178 if (command instanceof PlayPauseType) {
179 if (command.equals(PlayPauseType.PLAY)) {
180 connection.playerPlayPause();
181 } else if (command.equals(PlayPauseType.PAUSE)) {
182 connection.playerPlayPause();
184 } else if (command instanceof NextPreviousType) {
185 if (command.equals(NextPreviousType.NEXT)) {
186 connection.playerNext();
187 } else if (command.equals(NextPreviousType.PREVIOUS)) {
188 connection.playerPrevious();
190 } else if (command instanceof RewindFastforwardType) {
191 if (command.equals(RewindFastforwardType.REWIND)) {
192 connection.playerRewind();
193 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
194 connection.playerFastForward();
196 } else if (RefreshType.REFRESH == command) {
197 connection.updatePlayerStatus();
201 if (command.equals(OnOffType.ON)) {
203 } else if (RefreshType.REFRESH == command) {
204 connection.updatePlayerStatus();
207 case CHANNEL_PLAYURI:
208 if (command instanceof StringType) {
210 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
211 } else if (RefreshType.REFRESH == command) {
212 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
215 case CHANNEL_PLAYNOTIFICATION:
216 if (command instanceof StringType) {
217 playNotificationSoundURI((StringType) command);
218 updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
219 } else if (command.equals(RefreshType.REFRESH)) {
220 updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
223 case CHANNEL_PLAYFAVORITE:
224 if (command instanceof StringType) {
225 playFavorite(command);
226 updateState(favoriteChannelUID, UnDefType.UNDEF);
227 } else if (RefreshType.REFRESH == command) {
228 updateState(favoriteChannelUID, UnDefType.UNDEF);
231 case CHANNEL_PVR_OPEN_TV:
232 if (command instanceof StringType) {
233 playPVRChannel(command, PVR_TV, CHANNEL_PVR_OPEN_TV);
234 updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
235 } else if (RefreshType.REFRESH == command) {
236 updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
239 case CHANNEL_PVR_OPEN_RADIO:
240 if (command instanceof StringType) {
241 playPVRChannel(command, PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
242 updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
243 } else if (RefreshType.REFRESH == command) {
244 updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
247 case CHANNEL_SHOWNOTIFICATION:
248 showNotification(channelUID, command);
251 if (command instanceof StringType) {
252 connection.input(command.toString());
253 updateState(CHANNEL_INPUT, UnDefType.UNDEF);
254 } else if (RefreshType.REFRESH == command) {
255 updateState(CHANNEL_INPUT, UnDefType.UNDEF);
258 case CHANNEL_INPUTTEXT:
259 if (command instanceof StringType) {
260 connection.inputText(command.toString());
261 updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
262 } else if (RefreshType.REFRESH == command) {
263 updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
266 case CHANNEL_INPUTACTION:
267 if (command instanceof StringType) {
268 connection.inputAction(command.toString());
269 updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
270 } else if (RefreshType.REFRESH == command) {
271 updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
274 case CHANNEL_INPUTBUTTONEVENT:
275 logger.debug("handleCommand CHANNEL_INPUTBUTTONEVENT {}.", command);
276 if (command instanceof StringType) {
277 connection.inputButtonEvent(command.toString());
278 updateState(CHANNEL_INPUTBUTTONEVENT, UnDefType.UNDEF);
279 } else if (RefreshType.REFRESH == command) {
280 updateState(CHANNEL_INPUTBUTTONEVENT, UnDefType.UNDEF);
283 case CHANNEL_SYSTEMCOMMAND:
284 if (command instanceof StringType) {
285 handleSystemCommand(command.toString());
286 updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
287 } else if (RefreshType.REFRESH == command) {
288 updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
291 case CHANNEL_PROFILE:
292 if (command instanceof StringType) {
293 connection.profile(command.toString());
294 } else if (RefreshType.REFRESH == command) {
295 connection.updateCurrentProfile();
301 case CHANNEL_SHOWTITLE:
302 case CHANNEL_MEDIATYPE:
303 case CHANNEL_GENRELIST:
304 case CHANNEL_PVR_CHANNEL:
305 case CHANNEL_THUMBNAIL:
307 case CHANNEL_AUDIO_CODEC:
309 case CHANNEL_AUDIO_INDEX:
310 if (command instanceof DecimalType) {
311 connection.setAudioStream(((DecimalType) command).intValue());
314 case CHANNEL_VIDEO_CODEC:
315 case CHANNEL_VIDEO_INDEX:
316 if (command instanceof DecimalType) {
317 connection.setVideoStream(((DecimalType) command).intValue());
320 case CHANNEL_SUBTITLE_ENABLED:
321 if (command.equals(OnOffType.ON)) {
322 connection.setSubtitleEnabled(true);
323 } else if (command.equals(OnOffType.OFF)) {
324 connection.setSubtitleEnabled(false);
327 case CHANNEL_SUBTITLE_INDEX:
328 if (command instanceof DecimalType) {
329 connection.setSubtitle(((DecimalType) command).intValue());
332 case CHANNEL_CURRENTTIME:
333 if (command instanceof QuantityType) {
334 connection.setTime(((QuantityType<?>) command).intValue());
337 case CHANNEL_CURRENTTIMEPERCENTAGE:
338 case CHANNEL_DURATION:
339 if (RefreshType.REFRESH == command) {
340 connection.updatePlayerStatus();
344 Channel channel = getThing().getChannel(channelUID);
345 if (channel != null) {
346 ChannelTypeUID ctuid = channel.getChannelTypeUID();
348 if (ctuid.getId().equals(CHANNEL_TYPE_SHOWNOTIFICATION)) {
349 showNotification(channelUID, command);
354 logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup());
359 private void showNotification(ChannelUID channelUID, Command command) {
360 if (command instanceof StringType) {
361 Channel channel = getThing().getChannel(channelUID);
362 if (channel != null) {
363 String title = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_TITLE);
364 BigDecimal displayTime = (BigDecimal) channel.getConfiguration()
365 .get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_DISPLAYTIME);
366 String icon = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_ICON);
367 connection.showNotification(title, displayTime, icon, command.toString());
369 updateState(channelUID, UnDefType.UNDEF);
370 } else if (RefreshType.REFRESH == command) {
371 updateState(channelUID, UnDefType.UNDEF);
375 private URI getImageBaseUrl() throws URISyntaxException {
376 KodiConfig config = getConfigAs(KodiConfig.class);
377 String host = config.getIpAddress();
378 int httpPort = config.getHttpPort();
379 String httpUser = config.getHttpUser();
380 String httpPassword = config.getHttpPassword();
381 String userInfo = httpUser == null || httpUser.isEmpty() || httpPassword == null || httpPassword.isEmpty()
383 : String.format("%s:%s", httpUser, httpPassword);
384 return new URI("http", userInfo, host, httpPort, "/image/", null, null);
388 connection.playerStop();
391 public void playURI(Command command) {
392 connection.playURI(command.toString());
395 private void playFavorite(Command command) {
396 KodiFavorite favorite = connection.getFavorite(command.toString());
397 if (favorite != null) {
398 String path = favorite.getPath();
399 String windowParameter = favorite.getWindowParameter();
400 if (path != null && !path.isEmpty()) {
401 connection.playURI(path);
402 } else if (windowParameter != null && !windowParameter.isEmpty()) {
403 String[] windowParameters = { windowParameter };
404 connection.activateWindow(favorite.getWindow(), windowParameters);
406 connection.activateWindow(favorite.getWindow());
409 logger.debug("Received unknown favorite '{}'.", command);
413 public void playPVRChannel(final Command command, final String pvrChannelType, final String channelId) {
414 int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
415 int pvrChannelId = connection.getPVRChannelId(pvrChannelGroupId, command.toString());
416 if (pvrChannelId > 0) {
417 connection.playPVRChannel(pvrChannelId);
419 logger.debug("Received unknown PVR channel '{}'.", command);
423 private int getPVRChannelGroupId(final String pvrChannelType, final String channelId) {
424 Channel channel = getThing().getChannel(channelId);
425 if (channel != null) {
426 KodiChannelConfig config = channel.getConfiguration().as(KodiChannelConfig.class);
427 String pvrChannelGroupName = config.getGroup();
428 int pvrChannelGroupId = connection.getPVRChannelGroupId(pvrChannelType, pvrChannelGroupName);
429 if (pvrChannelGroupId <= 0) {
430 logger.debug("Received unknown PVR channel group '{}'. Using default.", pvrChannelGroupName);
431 pvrChannelGroupId = PVR_TV.equals(pvrChannelType) ? 1 : 2;
433 return pvrChannelGroupId;
438 private void handleSystemCommand(String command) {
440 case SYSTEM_COMMAND_QUIT:
441 connection.sendApplicationQuit();
443 case SYSTEM_COMMAND_HIBERNATE:
444 case SYSTEM_COMMAND_REBOOT:
445 case SYSTEM_COMMAND_SHUTDOWN:
446 case SYSTEM_COMMAND_SUSPEND:
447 connection.sendSystemCommand(command);
450 logger.debug("Received unknown system command '{}'.", command);
456 * Play the notification by 1) saving the state of the player, 2) stopping the current
457 * playlist item, 3) adding the notification as a new playlist item, 4) playing the new
458 * playlist item, and 5) restoring the player to its previous state.
460 public void playNotificationSoundURI(StringType uri) {
461 // save the current state of the player
462 logger.trace("Saving current player state");
463 KodiPlayerState playerState = new KodiPlayerState();
464 playerState.setSavedVolume(connection.getVolume());
465 playerState.setPlaylistID(connection.getActivePlaylist());
466 playerState.setSavedState(connection.getState());
468 int audioPlaylistID = connection.getPlaylistID("audio");
469 int videoPlaylistID = connection.getPlaylistID("video");
472 if (KodiState.PLAY.equals(connection.getState())) {
473 // pause if current media is "audio" or "video", stop otherwise
474 if (audioPlaylistID == playerState.getSavedPlaylistID()
475 || videoPlaylistID == playerState.getSavedPlaylistID()) {
476 connection.playerPlayPause();
477 waitForState(KodiState.PAUSE);
479 connection.playerStop();
480 waitForState(KodiState.STOP);
484 // set notification sound volume
485 logger.trace("Setting up player for notification");
486 int notificationVolume = getNotificationSoundVolume().intValue();
487 connection.setVolume(notificationVolume);
488 waitForVolume(notificationVolume);
490 // add the notification uri to the playlist and play it
491 logger.trace("Playing notification");
492 connection.playlistInsert(audioPlaylistID, uri.toString(), 0);
493 waitForPlaylistState(KodiPlaylistState.ADDED);
495 connection.playlistPlay(audioPlaylistID, 0);
496 waitForState(KodiState.PLAY);
497 // wait for stop if previous playlist wasn't "audio"
498 if (audioPlaylistID != playerState.getSavedPlaylistID()) {
499 waitForState(KodiState.STOP);
502 // remove the notification uri from the playlist
503 connection.playlistRemove(audioPlaylistID, 0);
504 waitForPlaylistState(KodiPlaylistState.REMOVED);
506 // restore previous volume
507 connection.setVolume(playerState.getSavedVolume());
508 waitForVolume(playerState.getSavedVolume());
510 // resume playing save playlist item if player wasn't stopped
511 logger.trace("Restoring player state");
512 switch (playerState.getSavedState()) {
514 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
515 connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
519 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
520 connection.playerPlayPause();
533 * Wait for the volume status to equal the targetVolume
535 private boolean waitForVolume(int targetVolume) {
536 int timeoutMaxCount = 20, timeoutCount = 0;
537 logger.trace("Waiting up to {} ms for the volume to be updated ...", timeoutMaxCount * 100);
538 while (targetVolume != connection.getVolume() && timeoutCount < timeoutMaxCount) {
541 } catch (InterruptedException e) {
546 return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
550 * Wait for the player state so that we know when the notification has started or finished playing
552 private boolean waitForState(KodiState state) {
553 int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
554 logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 100, state);
555 while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
558 } catch (InterruptedException e) {
563 return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
567 * Wait for the playlist state so that we know when the notification has started or finished playing
569 private boolean waitForPlaylistState(KodiPlaylistState playlistState) {
570 int timeoutMaxCount = 20, timeoutCount = 0;
571 logger.trace("Waiting up to {} ms for playlist state '{}' to be set ...", timeoutMaxCount * 100, playlistState);
572 while (!playlistState.equals(connection.getPlaylistState()) && timeoutCount < timeoutMaxCount) {
575 } catch (InterruptedException e) {
580 return checkForTimeout(timeoutCount, timeoutMaxCount,
581 "playlist state to '" + playlistState.toString() + "' be set");
585 * Log timeout for wait
587 private boolean checkForTimeout(int timeoutCount, int timeoutLimit, String message) {
588 if (timeoutCount >= timeoutLimit) {
589 logger.debug("TIMEOUT after {} ms waiting for {}!", timeoutCount * 100, message);
592 logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
598 * Gets the current volume level
600 public PercentType getVolume() {
601 return new PercentType(connection.getVolume());
605 * Sets the volume level
607 * @param volume Volume to be set
609 public void setVolume(PercentType volume) {
610 if (volume != null) {
611 connection.setVolume(volume.intValue());
616 * Gets the volume level for a notification sound
618 public PercentType getNotificationSoundVolume() {
619 Integer notificationSoundVolume = getConfigAs(KodiConfig.class).getNotificationVolume();
620 if (notificationSoundVolume == null) {
621 // if no value is set we use the current volume instead
622 return new PercentType(connection.getVolume());
624 return new PercentType(notificationSoundVolume);
628 * Sets the volume level for a notification sound
630 * @param notificationSoundVolume Volume to be set
632 public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
633 if (notificationSoundVolume != null) {
634 connection.setVolume(notificationSoundVolume.intValue());
639 public void initialize() {
641 String host = getConfig().get(HOST_PARAMETER).toString();
642 if (host == null || host.isEmpty()) {
643 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
644 "No network address specified");
646 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
648 connectionCheckerFuture = scheduler.scheduleWithFixedDelay(() -> {
649 if (connection.checkConnection()) {
650 updateFavoriteChannelStateDescription();
651 updatePVRChannelStateDescription(PVR_TV, CHANNEL_PVR_OPEN_TV);
652 updatePVRChannelStateDescription(PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
653 updateProfileStateDescription();
655 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
656 "No connection established");
658 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
660 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
661 if (KodiState.PLAY.equals(connection.getState())) {
662 connection.updatePlayerStatus();
664 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
666 } catch (Exception e) {
667 logger.debug("error during opening connection: {}", e.getMessage(), e);
668 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
672 private void updateFavoriteChannelStateDescription() {
673 if (isLinked(favoriteChannelUID)) {
674 List<StateOption> options = new ArrayList<>();
675 for (KodiFavorite favorite : connection.getFavorites()) {
676 options.add(new StateOption(favorite.getTitle(), favorite.getTitle()));
678 stateDescriptionProvider.setStateOptions(favoriteChannelUID, options);
682 private void updatePVRChannelStateDescription(final String pvrChannelType, final String channelId) {
683 if (isLinked(channelId)) {
684 int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
685 List<StateOption> options = new ArrayList<>();
686 for (KodiPVRChannel pvrChannel : connection.getPVRChannels(pvrChannelGroupId)) {
687 options.add(new StateOption(pvrChannel.getLabel(), pvrChannel.getLabel()));
689 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
693 private void updateProfileStateDescription() {
694 if (isLinked(profileChannelUID)) {
695 List<StateOption> options = new ArrayList<>();
696 for (KodiProfile profile : connection.getProfiles()) {
697 options.add(new StateOption(profile.getLabel(), profile.getLabel()));
699 stateDescriptionProvider.setStateOptions(profileChannelUID, options);
704 public void updateAudioStreamOptions(List<KodiAudioStream> audios) {
705 if (isLinked(CHANNEL_AUDIO_INDEX)) {
706 List<StateOption> options = new ArrayList<>();
707 for (KodiAudioStream audio : audios) {
708 options.add(new StateOption(Integer.toString(audio.getIndex()),
709 audio.getLanguage() + " [" + audio.getName() + "] (" + audio.getCodec() + "-"
710 + Integer.toString(audio.getChannels()) + " "
711 + Integer.toString(audio.getBitrate() / 1000) + "kb/s)"));
713 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
718 public void updateSubtitleOptions(List<KodiSubtitle> subtitles) {
719 if (isLinked(CHANNEL_SUBTITLE_INDEX)) {
720 List<StateOption> options = new ArrayList<>();
721 for (KodiSubtitle subtitle : subtitles) {
722 options.add(new StateOption(Integer.toString(subtitle.getIndex()),
723 subtitle.getLanguage() + " [" + subtitle.getName() + "]"));
725 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
731 public void updateConnectionState(boolean connected) {
733 updateStatus(ThingStatus.ONLINE);
734 scheduler.schedule(() -> connection.getSystemProperties(), 1, TimeUnit.SECONDS);
735 if (isLinked(volumeChannelUID) || isLinked(mutedChannelUID)) {
736 scheduler.schedule(() -> connection.updateVolume(), 1, TimeUnit.SECONDS);
738 if (isLinked(profileChannelUID)) {
739 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
742 String version = connection.getVersion();
743 thing.setProperty(PROPERTY_VERSION, version);
744 } catch (Exception e) {
745 logger.debug("error during reading version: {}", e.getMessage(), e);
748 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
753 public void updateScreenSaverState(boolean screenSaverActive) {
754 updateState(screenSaverChannelUID, OnOffType.from(screenSaverActive));
758 public void updateInputRequestedState(boolean inputRequested) {
759 updateState(inputRequestedChannelUID, OnOffType.from(inputRequested));
763 public void updatePlaylistState(KodiPlaylistState playlistState) {
767 public void updateVolume(int volume) {
768 updateState(volumeChannelUID, new PercentType(volume));
772 public void updatePlayerState(KodiState state) {
775 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
776 updateState(CHANNEL_STOP, OnOffType.OFF);
779 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
780 updateState(CHANNEL_STOP, OnOffType.OFF);
784 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
785 updateState(CHANNEL_STOP, OnOffType.ON);
788 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
789 updateState(CHANNEL_STOP, OnOffType.OFF);
792 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
793 updateState(CHANNEL_STOP, OnOffType.OFF);
799 public void updateMuted(boolean muted) {
800 updateState(mutedChannelUID, OnOffType.from(muted));
804 public void updateTitle(String title) {
805 updateState(CHANNEL_TITLE, createStringState(title));
809 public void updateOriginalTitle(String title) {
810 updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
814 public void updateShowTitle(String title) {
815 updateState(CHANNEL_SHOWTITLE, createStringState(title));
819 public void updateAlbum(String album) {
820 updateState(CHANNEL_ALBUM, createStringState(album));
824 public void updateArtistList(List<String> artistList) {
825 updateState(CHANNEL_ARTIST, createStringListState(artistList));
829 public void updateMediaFile(String mediaFile) {
830 updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
834 public void updateMediaType(String mediaType) {
835 updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
839 public void updateMediaID(int mediaid) {
840 updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
844 public void updateRating(double rating) {
845 updateState(CHANNEL_RATING, new DecimalType(rating));
849 public void updateUserRating(double rating) {
850 updateState(CHANNEL_USERRATING, new DecimalType(rating));
854 public void updateMpaa(String mpaa) {
855 updateState(CHANNEL_MPAA, createStringState(mpaa));
859 public void updateUniqueIDDouban(String uniqueid) {
860 updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
864 public void updateUniqueIDImdb(String uniqueid) {
865 updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
869 public void updateUniqueIDTmdb(String uniqueid) {
870 updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
874 public void updateUniqueIDImdbtvshow(String uniqueid) {
875 updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
879 public void updateUniqueIDTmdbtvshow(String uniqueid) {
880 updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
884 public void updateUniqueIDTmdbepisode(String uniqueid) {
885 updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
889 public void updateSeason(int season) {
890 updateState(CHANNEL_SEASON, new DecimalType(season));
894 public void updateEpisode(int episode) {
895 updateState(CHANNEL_EPISODE, new DecimalType(episode));
899 public void updateGenreList(List<String> genreList) {
900 updateState(CHANNEL_GENRELIST, createStringListState(genreList));
904 public void updatePVRChannel(String channel) {
905 updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
909 public void updateThumbnail(RawType thumbnail) {
910 updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
914 public void updateFanart(RawType fanart) {
915 updateState(CHANNEL_FANART, createImageState(fanart));
919 public void updateAudioCodec(String codec) {
920 updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
924 public void updateAudioIndex(int index) {
925 updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
929 public void updateAudioChannels(int channels) {
930 updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
934 public void updateAudioLanguage(String language) {
935 updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
939 public void updateAudioName(String name) {
940 updateState(CHANNEL_AUDIO_NAME, createStringState(name));
944 public void updateVideoCodec(String codec) {
945 updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
949 public void updateVideoIndex(int index) {
950 updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
954 public void updateVideoHeight(int height) {
955 updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
959 public void updateVideoWidth(int width) {
960 updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
964 public void updateSubtitleEnabled(boolean enabled) {
965 updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.from(enabled));
969 public void updateSubtitleIndex(int index) {
970 updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
974 public void updateSubtitleLanguage(String language) {
975 updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
979 public void updateSubtitleName(String name) {
980 updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
984 public void updateCurrentTime(long currentTime) {
985 updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, Units.SECOND));
989 public void updateCurrentTimePercentage(double currentTimePercentage) {
990 updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, Units.PERCENT));
994 public void updateDuration(long duration) {
995 updateState(CHANNEL_DURATION, createQuantityState(duration, Units.SECOND));
999 public void updateCurrentProfile(String profile) {
1000 updateState(profileChannelUID, new StringType(profile));
1004 public void updateSystemProperties(KodiSystemProperties systemProperties) {
1005 if (systemProperties != null) {
1006 List<CommandOption> options = new ArrayList<>();
1007 if (systemProperties.canHibernate()) {
1008 options.add(new CommandOption(SYSTEM_COMMAND_HIBERNATE, SYSTEM_COMMAND_HIBERNATE));
1010 if (systemProperties.canReboot()) {
1011 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
1013 if (systemProperties.canShutdown()) {
1014 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
1016 if (systemProperties.canSuspend()) {
1017 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
1019 if (systemProperties.canQuit()) {
1020 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
1022 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1028 * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1030 private State createStringState(String string) {
1031 if (string == null || string.isEmpty()) {
1032 return UnDefType.UNDEF;
1034 return new StringType(string);
1039 * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1042 private State createStringListState(List<String> list) {
1043 if (list == null || list.isEmpty()) {
1044 return UnDefType.UNDEF;
1046 return createStringState(list.stream().collect(Collectors.joining(", ")));
1051 * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1053 private State createImageState(@Nullable RawType image) {
1054 if (image == null) {
1055 return UnDefType.UNDEF;
1061 private State createQuantityState(Number value, Unit<?> unit) {
1062 return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);