2 * Copyright (c) 2010-2020 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.SmartHomeUnits;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.BaseThingHandler;
60 import org.openhab.core.thing.type.ChannelTypeUID;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.CommandOption;
63 import org.openhab.core.types.RefreshType;
64 import org.openhab.core.types.State;
65 import org.openhab.core.types.StateOption;
66 import org.openhab.core.types.UnDefType;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
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 profileChannelUID;
96 private ScheduledFuture<?> connectionCheckerFuture;
97 private ScheduledFuture<?> statusUpdaterFuture;
99 public KodiHandler(Thing thing, KodiDynamicCommandDescriptionProvider commandDescriptionProvider,
100 KodiDynamicStateDescriptionProvider stateDescriptionProvider, WebSocketClient webSocketClient,
101 String callbackUrl) {
103 connection = new KodiConnection(this, webSocketClient, callbackUrl);
105 this.commandDescriptionProvider = commandDescriptionProvider;
106 this.stateDescriptionProvider = stateDescriptionProvider;
108 profileChannelUID = new ChannelUID(getThing().getUID(), CHANNEL_PROFILE);
112 public void dispose() {
114 if (connectionCheckerFuture != null) {
115 connectionCheckerFuture.cancel(true);
117 if (statusUpdaterFuture != null) {
118 statusUpdaterFuture.cancel(true);
120 if (connection != null) {
125 private int getIntConfigParameter(String key, int defaultValue) {
126 Object obj = this.getConfig().get(key);
127 if (obj instanceof Number) {
128 return ((Number) obj).intValue();
129 } else if (obj instanceof String) {
130 return Integer.parseInt(obj.toString());
136 public void handleCommand(ChannelUID channelUID, Command command) {
137 switch (channelUID.getIdWithoutGroup()) {
139 if (command.equals(OnOffType.ON)) {
140 connection.setMute(true);
141 } else if (command.equals(OnOffType.OFF)) {
142 connection.setMute(false);
143 } else if (RefreshType.REFRESH == command) {
144 connection.updateVolume();
148 if (command instanceof PercentType) {
149 connection.setVolume(((PercentType) command).intValue());
150 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
151 connection.increaseVolume();
152 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
153 connection.decreaseVolume();
154 } else if (command.equals(OnOffType.OFF)) {
155 connection.setVolume(0);
156 } else if (command.equals(OnOffType.ON)) {
157 connection.setVolume(100);
158 } else if (RefreshType.REFRESH == command) {
159 connection.updateVolume();
162 case CHANNEL_CONTROL:
163 if (command instanceof PlayPauseType) {
164 if (command.equals(PlayPauseType.PLAY)) {
165 connection.playerPlayPause();
166 } else if (command.equals(PlayPauseType.PAUSE)) {
167 connection.playerPlayPause();
169 } else if (command instanceof NextPreviousType) {
170 if (command.equals(NextPreviousType.NEXT)) {
171 connection.playerNext();
172 } else if (command.equals(NextPreviousType.PREVIOUS)) {
173 connection.playerPrevious();
175 } else if (command instanceof RewindFastforwardType) {
176 if (command.equals(RewindFastforwardType.REWIND)) {
177 connection.playerRewind();
178 } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
179 connection.playerFastForward();
181 } else if (RefreshType.REFRESH == command) {
182 connection.updatePlayerStatus();
186 if (command.equals(OnOffType.ON)) {
188 } else if (RefreshType.REFRESH == command) {
189 connection.updatePlayerStatus();
192 case CHANNEL_PLAYURI:
193 if (command instanceof StringType) {
195 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
196 } else if (RefreshType.REFRESH == command) {
197 updateState(CHANNEL_PLAYURI, UnDefType.UNDEF);
200 case CHANNEL_PLAYNOTIFICATION:
201 if (command instanceof StringType) {
202 playNotificationSoundURI((StringType) command);
203 updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
204 } else if (command.equals(RefreshType.REFRESH)) {
205 updateState(CHANNEL_PLAYNOTIFICATION, UnDefType.UNDEF);
208 case CHANNEL_PLAYFAVORITE:
209 if (command instanceof StringType) {
210 playFavorite(command);
211 updateState(CHANNEL_PLAYFAVORITE, UnDefType.UNDEF);
212 } else if (RefreshType.REFRESH == command) {
213 updateState(CHANNEL_PLAYFAVORITE, UnDefType.UNDEF);
216 case CHANNEL_PVR_OPEN_TV:
217 if (command instanceof StringType) {
218 playPVRChannel(command, PVR_TV, CHANNEL_PVR_OPEN_TV);
219 updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
220 } else if (RefreshType.REFRESH == command) {
221 updateState(CHANNEL_PVR_OPEN_TV, UnDefType.UNDEF);
224 case CHANNEL_PVR_OPEN_RADIO:
225 if (command instanceof StringType) {
226 playPVRChannel(command, PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
227 updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
228 } else if (RefreshType.REFRESH == command) {
229 updateState(CHANNEL_PVR_OPEN_RADIO, UnDefType.UNDEF);
232 case CHANNEL_SHOWNOTIFICATION:
233 showNotification(channelUID, command);
236 if (command instanceof StringType) {
237 connection.input(command.toString());
238 updateState(CHANNEL_INPUT, UnDefType.UNDEF);
239 } else if (RefreshType.REFRESH == command) {
240 updateState(CHANNEL_INPUT, UnDefType.UNDEF);
243 case CHANNEL_INPUTTEXT:
244 if (command instanceof StringType) {
245 connection.inputText(command.toString());
246 updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
247 } else if (RefreshType.REFRESH == command) {
248 updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF);
251 case CHANNEL_INPUTACTION:
252 if (command instanceof StringType) {
253 connection.inputAction(command.toString());
254 updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
255 } else if (RefreshType.REFRESH == command) {
256 updateState(CHANNEL_INPUTACTION, UnDefType.UNDEF);
259 case CHANNEL_SYSTEMCOMMAND:
260 if (command instanceof StringType) {
261 handleSystemCommand(command.toString());
262 updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
263 } else if (RefreshType.REFRESH == command) {
264 updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF);
267 case CHANNEL_PROFILE:
268 if (command instanceof StringType) {
269 connection.profile(command.toString());
275 case CHANNEL_SHOWTITLE:
276 case CHANNEL_MEDIATYPE:
277 case CHANNEL_GENRELIST:
278 case CHANNEL_PVR_CHANNEL:
279 case CHANNEL_THUMBNAIL:
281 case CHANNEL_AUDIO_CODEC:
283 case CHANNEL_AUDIO_INDEX:
284 if (command instanceof DecimalType) {
285 connection.setAudioStream(((DecimalType) command).intValue());
288 case CHANNEL_VIDEO_CODEC:
289 case CHANNEL_VIDEO_INDEX:
290 if (command instanceof DecimalType) {
291 connection.setVideoStream(((DecimalType) command).intValue());
294 case CHANNEL_SUBTITLE_ENABLED:
295 if (command.equals(OnOffType.ON)) {
296 connection.setSubtitleEnabled(true);
297 } else if (command.equals(OnOffType.OFF)) {
298 connection.setSubtitleEnabled(false);
301 case CHANNEL_SUBTITLE_INDEX:
302 if (command instanceof DecimalType) {
303 connection.setSubtitle(((DecimalType) command).intValue());
306 case CHANNEL_CURRENTTIME:
307 if (command instanceof QuantityType) {
308 connection.setTime(((QuantityType<?>) command).intValue());
311 case CHANNEL_CURRENTTIMEPERCENTAGE:
312 case CHANNEL_DURATION:
313 if (RefreshType.REFRESH == command) {
314 connection.updatePlayerStatus();
318 Channel channel = getThing().getChannel(channelUID);
319 if (channel != null) {
320 ChannelTypeUID ctuid = channel.getChannelTypeUID();
322 if (ctuid.getId().equals(CHANNEL_TYPE_SHOWNOTIFICATION)) {
323 showNotification(channelUID, command);
328 logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup());
333 private void showNotification(ChannelUID channelUID, Command command) {
334 if (command instanceof StringType) {
335 Channel channel = getThing().getChannel(channelUID);
336 if (channel != null) {
337 String title = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_TITLE);
338 BigDecimal displayTime = (BigDecimal) channel.getConfiguration()
339 .get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_DISPLAYTIME);
340 String icon = (String) channel.getConfiguration().get(CHANNEL_TYPE_SHOWNOTIFICATION_PARAM_ICON);
341 connection.showNotification(title, displayTime, icon, command.toString());
343 updateState(channelUID, UnDefType.UNDEF);
344 } else if (RefreshType.REFRESH == command) {
345 updateState(channelUID, UnDefType.UNDEF);
349 private URI getImageBaseUrl() throws URISyntaxException {
350 KodiConfig config = getConfigAs(KodiConfig.class);
351 String host = config.getIpAddress();
352 int httpPort = config.getHttpPort();
353 String httpUser = config.getHttpUser();
354 String httpPassword = config.getHttpPassword();
355 String userInfo = httpUser == null || httpUser.isEmpty() || httpPassword == null || httpPassword.isEmpty()
357 : String.format("%s:%s", httpUser, httpPassword);
358 return new URI("http", userInfo, host, httpPort, "/image/", null, null);
362 connection.playerStop();
365 public void playURI(Command command) {
366 connection.playURI(command.toString());
369 private void playFavorite(Command command) {
370 KodiFavorite favorite = connection.getFavorite(command.toString());
371 if (favorite != null) {
372 String path = favorite.getPath();
373 String windowParameter = favorite.getWindowParameter();
374 if (path != null && !path.isEmpty()) {
375 connection.playURI(path);
376 } else if (windowParameter != null && !windowParameter.isEmpty()) {
377 String[] windowParameters = { windowParameter };
378 connection.activateWindow(favorite.getWindow(), windowParameters);
380 connection.activateWindow(favorite.getWindow());
383 logger.debug("Received unknown favorite '{}'.", command);
387 public void playPVRChannel(final Command command, final String pvrChannelType, final String channelId) {
388 int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
389 int pvrChannelId = connection.getPVRChannelId(pvrChannelGroupId, command.toString());
390 if (pvrChannelId > 0) {
391 connection.playPVRChannel(pvrChannelId);
393 logger.debug("Received unknown PVR channel '{}'.", command);
397 private int getPVRChannelGroupId(final String pvrChannelType, final String channelId) {
398 Channel channel = getThing().getChannel(channelId);
399 if (channel != null) {
400 KodiChannelConfig config = channel.getConfiguration().as(KodiChannelConfig.class);
401 String pvrChannelGroupName = config.getGroup();
402 int pvrChannelGroupId = connection.getPVRChannelGroupId(pvrChannelType, pvrChannelGroupName);
403 if (pvrChannelGroupId <= 0) {
404 logger.debug("Received unknown PVR channel group '{}'. Using default.", pvrChannelGroupName);
405 pvrChannelGroupId = PVR_TV.equals(pvrChannelType) ? 1 : 2;
407 return pvrChannelGroupId;
412 private void handleSystemCommand(String command) {
414 case SYSTEM_COMMAND_QUIT:
415 connection.sendApplicationQuit();
417 case SYSTEM_COMMAND_HIBERNATE:
418 case SYSTEM_COMMAND_REBOOT:
419 case SYSTEM_COMMAND_SHUTDOWN:
420 case SYSTEM_COMMAND_SUSPEND:
421 connection.sendSystemCommand(command);
424 logger.debug("Received unknown system command '{}'.", command);
430 * Play the notification by 1) saving the state of the player, 2) stopping the current
431 * playlist item, 3) adding the notification as a new playlist item, 4) playing the new
432 * playlist item, and 5) restoring the player to its previous state.
434 public void playNotificationSoundURI(StringType uri) {
435 // save the current state of the player
436 logger.trace("Saving current player state");
437 KodiPlayerState playerState = new KodiPlayerState();
438 playerState.setSavedVolume(connection.getVolume());
439 playerState.setPlaylistID(connection.getActivePlaylist());
440 playerState.setSavedState(connection.getState());
442 int audioPlaylistID = connection.getPlaylistID("audio");
443 int videoPlaylistID = connection.getPlaylistID("video");
446 if (KodiState.PLAY.equals(connection.getState())) {
447 // pause if current media is "audio" or "video", stop otherwise
448 if (audioPlaylistID == playerState.getSavedPlaylistID()
449 || videoPlaylistID == playerState.getSavedPlaylistID()) {
450 connection.playerPlayPause();
451 waitForState(KodiState.PAUSE);
453 connection.playerStop();
454 waitForState(KodiState.STOP);
458 // set notification sound volume
459 logger.trace("Setting up player for notification");
460 int notificationVolume = getNotificationSoundVolume().intValue();
461 connection.setVolume(notificationVolume);
462 waitForVolume(notificationVolume);
464 // add the notification uri to the playlist and play it
465 logger.trace("Playing notification");
466 connection.playlistInsert(audioPlaylistID, uri.toString(), 0);
467 waitForPlaylistState(KodiPlaylistState.ADDED);
469 connection.playlistPlay(audioPlaylistID, 0);
470 waitForState(KodiState.PLAY);
471 // wait for stop if previous playlist wasn't "audio"
472 if (audioPlaylistID != playerState.getSavedPlaylistID()) {
473 waitForState(KodiState.STOP);
476 // remove the notification uri from the playlist
477 connection.playlistRemove(audioPlaylistID, 0);
478 waitForPlaylistState(KodiPlaylistState.REMOVED);
480 // restore previous volume
481 connection.setVolume(playerState.getSavedVolume());
482 waitForVolume(playerState.getSavedVolume());
484 // resume playing save playlist item if player wasn't stopped
485 logger.trace("Restoring player state");
486 switch (playerState.getSavedState()) {
488 if (audioPlaylistID != playerState.getSavedPlaylistID() && -1 != playerState.getSavedPlaylistID()) {
489 connection.playlistPlay(playerState.getSavedPlaylistID(), 0);
493 if (audioPlaylistID == playerState.getSavedPlaylistID()) {
494 connection.playerPlayPause();
507 * Wait for the volume status to equal the targetVolume
509 private boolean waitForVolume(int targetVolume) {
510 int timeoutMaxCount = 20, timeoutCount = 0;
511 logger.trace("Waiting up to {} ms for the volume to be updated ...", timeoutMaxCount * 100);
512 while (targetVolume != connection.getVolume() && timeoutCount < timeoutMaxCount) {
515 } catch (InterruptedException e) {
520 return checkForTimeout(timeoutCount, timeoutMaxCount, "volume to be updated");
524 * Wait for the player state so that we know when the notification has started or finished playing
526 private boolean waitForState(KodiState state) {
527 int timeoutMaxCount = getConfigAs(KodiConfig.class).getNotificationTimeout().intValue(), timeoutCount = 0;
528 logger.trace("Waiting up to {} ms for state '{}' to be set ...", timeoutMaxCount * 100, state);
529 while (!state.equals(connection.getState()) && timeoutCount < timeoutMaxCount) {
532 } catch (InterruptedException e) {
537 return checkForTimeout(timeoutCount, timeoutMaxCount, "state to '" + state.toString() + "' be set");
541 * Wait for the playlist state so that we know when the notification has started or finished playing
543 private boolean waitForPlaylistState(KodiPlaylistState playlistState) {
544 int timeoutMaxCount = 20, timeoutCount = 0;
545 logger.trace("Waiting up to {} ms for playlist state '{}' to be set ...", timeoutMaxCount * 100, playlistState);
546 while (!playlistState.equals(connection.getPlaylistState()) && timeoutCount < timeoutMaxCount) {
549 } catch (InterruptedException e) {
554 return checkForTimeout(timeoutCount, timeoutMaxCount,
555 "playlist state to '" + playlistState.toString() + "' be set");
559 * Log timeout for wait
561 private boolean checkForTimeout(int timeoutCount, int timeoutLimit, String message) {
562 if (timeoutCount >= timeoutLimit) {
563 logger.debug("TIMEOUT after {} ms waiting for {}!", timeoutCount * 100, message);
566 logger.trace("Done waiting {} ms for {}", timeoutCount * 100, message);
572 * Gets the current volume level
574 public PercentType getVolume() {
575 return new PercentType(connection.getVolume());
579 * Sets the volume level
581 * @param volume Volume to be set
583 public void setVolume(PercentType volume) {
584 if (volume != null) {
585 connection.setVolume(volume.intValue());
590 * Gets the volume level for a notification sound
592 public PercentType getNotificationSoundVolume() {
593 Integer notificationSoundVolume = getConfigAs(KodiConfig.class).getNotificationVolume();
594 if (notificationSoundVolume == null) {
595 // if no value is set we use the current volume instead
596 return new PercentType(connection.getVolume());
598 return new PercentType(notificationSoundVolume);
602 * Sets the volume level for a notification sound
604 * @param notificationSoundVolume Volume to be set
606 public void setNotificationSoundVolume(PercentType notificationSoundVolume) {
607 if (notificationSoundVolume != null) {
608 connection.setVolume(notificationSoundVolume.intValue());
613 public void initialize() {
615 String host = getConfig().get(HOST_PARAMETER).toString();
616 if (host == null || host.isEmpty()) {
617 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
618 "No network address specified");
620 connection.connect(host, getIntConfigParameter(WS_PORT_PARAMETER, 9090), scheduler, getImageBaseUrl());
622 connectionCheckerFuture = scheduler.scheduleWithFixedDelay(() -> {
623 if (connection.checkConnection()) {
624 updateFavoriteChannelStateDescription();
625 updatePVRChannelStateDescription(PVR_TV, CHANNEL_PVR_OPEN_TV);
626 updatePVRChannelStateDescription(PVR_RADIO, CHANNEL_PVR_OPEN_RADIO);
627 updateProfileStateDescription();
629 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
630 "No connection established");
632 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
634 statusUpdaterFuture = scheduler.scheduleWithFixedDelay(() -> {
635 if (KodiState.PLAY.equals(connection.getState())) {
636 connection.updatePlayerStatus();
638 }, 1, getIntConfigParameter(REFRESH_PARAMETER, 10), TimeUnit.SECONDS);
640 } catch (Exception e) {
641 logger.debug("error during opening connection: {}", e.getMessage(), e);
642 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
646 private void updateFavoriteChannelStateDescription() {
647 if (isLinked(CHANNEL_PLAYFAVORITE)) {
648 List<StateOption> options = new ArrayList<>();
649 for (KodiFavorite favorite : connection.getFavorites()) {
650 options.add(new StateOption(favorite.getTitle(), favorite.getTitle()));
652 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PLAYFAVORITE),
657 private void updatePVRChannelStateDescription(final String pvrChannelType, final String channelId) {
658 if (isLinked(channelId)) {
659 int pvrChannelGroupId = getPVRChannelGroupId(pvrChannelType, channelId);
660 List<StateOption> options = new ArrayList<>();
661 for (KodiPVRChannel pvrChannel : connection.getPVRChannels(pvrChannelGroupId)) {
662 options.add(new StateOption(pvrChannel.getLabel(), pvrChannel.getLabel()));
664 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
668 private void updateProfileStateDescription() {
669 if (isLinked(profileChannelUID)) {
670 List<StateOption> options = new ArrayList<>();
671 for (KodiProfile profile : connection.getProfiles()) {
672 options.add(new StateOption(profile.getLabel(), profile.getLabel()));
674 stateDescriptionProvider.setStateOptions(profileChannelUID, options);
679 public void updateAudioStreamOptions(List<KodiAudioStream> audios) {
680 if (isLinked(CHANNEL_AUDIO_INDEX)) {
681 List<StateOption> options = new ArrayList<>();
682 for (KodiAudioStream audio : audios) {
683 options.add(new StateOption(Integer.toString(audio.getIndex()),
684 audio.getLanguage() + " [" + audio.getName() + "] (" + audio.getCodec() + "-"
685 + Integer.toString(audio.getChannels()) + " "
686 + Integer.toString(audio.getBitrate() / 1000) + "kb/s)"));
688 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_AUDIO_INDEX), options);
693 public void updateSubtitleOptions(List<KodiSubtitle> subtitles) {
694 if (isLinked(CHANNEL_SUBTITLE_INDEX)) {
695 List<StateOption> options = new ArrayList<>();
696 for (KodiSubtitle subtitle : subtitles) {
697 options.add(new StateOption(Integer.toString(subtitle.getIndex()),
698 subtitle.getLanguage() + " [" + subtitle.getName() + "]"));
700 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SUBTITLE_INDEX),
706 public void updateConnectionState(boolean connected) {
708 updateStatus(ThingStatus.ONLINE);
709 scheduler.schedule(() -> connection.getSystemProperties(), 1, TimeUnit.SECONDS);
710 scheduler.schedule(() -> connection.updateVolume(), 1, TimeUnit.SECONDS);
711 if (isLinked(profileChannelUID)) {
712 scheduler.schedule(() -> connection.updateCurrentProfile(), 1, TimeUnit.SECONDS);
715 String version = connection.getVersion();
716 thing.setProperty(PROPERTY_VERSION, version);
717 } catch (Exception e) {
718 logger.debug("error during reading version: {}", e.getMessage(), e);
721 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No connection established");
726 public void updateScreenSaverState(boolean screenSaveActive) {
730 public void updatePlaylistState(KodiPlaylistState playlistState) {
734 public void updateVolume(int volume) {
735 updateState(CHANNEL_VOLUME, new PercentType(volume));
739 public void updatePlayerState(KodiState state) {
742 updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
743 updateState(CHANNEL_STOP, OnOffType.OFF);
746 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
747 updateState(CHANNEL_STOP, OnOffType.OFF);
751 updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
752 updateState(CHANNEL_STOP, OnOffType.ON);
755 updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD);
756 updateState(CHANNEL_STOP, OnOffType.OFF);
759 updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND);
760 updateState(CHANNEL_STOP, OnOffType.OFF);
766 public void updateMuted(boolean muted) {
768 updateState(CHANNEL_MUTE, OnOffType.ON);
770 updateState(CHANNEL_MUTE, OnOffType.OFF);
775 public void updateTitle(String title) {
776 updateState(CHANNEL_TITLE, createStringState(title));
780 public void updateOriginalTitle(String title) {
781 updateState(CHANNEL_ORIGINALTITLE, createStringState(title));
785 public void updateShowTitle(String title) {
786 updateState(CHANNEL_SHOWTITLE, createStringState(title));
790 public void updateAlbum(String album) {
791 updateState(CHANNEL_ALBUM, createStringState(album));
795 public void updateArtistList(List<String> artistList) {
796 updateState(CHANNEL_ARTIST, createStringListState(artistList));
800 public void updateMediaFile(String mediaFile) {
801 updateState(CHANNEL_MEDIAFILE, createStringState(mediaFile));
805 public void updateMediaType(String mediaType) {
806 updateState(CHANNEL_MEDIATYPE, createStringState(mediaType));
810 public void updateMediaID(int mediaid) {
811 updateState(CHANNEL_MEDIAID, new DecimalType(mediaid));
815 public void updateRating(double rating) {
816 updateState(CHANNEL_RATING, new DecimalType(rating));
820 public void updateUserRating(double rating) {
821 updateState(CHANNEL_USERRATING, new DecimalType(rating));
825 public void updateMpaa(String mpaa) {
826 updateState(CHANNEL_MPAA, createStringState(mpaa));
830 public void updateUniqueIDDouban(String uniqueid) {
831 updateState(CHANNEL_UNIQUEID_DOUBAN, createStringState(uniqueid));
835 public void updateUniqueIDImdb(String uniqueid) {
836 updateState(CHANNEL_UNIQUEID_IMDB, createStringState(uniqueid));
840 public void updateUniqueIDTmdb(String uniqueid) {
841 updateState(CHANNEL_UNIQUEID_TMDB, createStringState(uniqueid));
845 public void updateUniqueIDImdbtvshow(String uniqueid) {
846 updateState(CHANNEL_UNIQUEID_IMDBTVSHOW, createStringState(uniqueid));
850 public void updateUniqueIDTmdbtvshow(String uniqueid) {
851 updateState(CHANNEL_UNIQUEID_TMDBTVSHOW, createStringState(uniqueid));
855 public void updateUniqueIDTmdbepisode(String uniqueid) {
856 updateState(CHANNEL_UNIQUEID_TMDBEPISODE, createStringState(uniqueid));
860 public void updateSeason(int season) {
861 updateState(CHANNEL_SEASON, new DecimalType(season));
865 public void updateEpisode(int episode) {
866 updateState(CHANNEL_EPISODE, new DecimalType(episode));
870 public void updateGenreList(List<String> genreList) {
871 updateState(CHANNEL_GENRELIST, createStringListState(genreList));
875 public void updatePVRChannel(String channel) {
876 updateState(CHANNEL_PVR_CHANNEL, createStringState(channel));
880 public void updateThumbnail(RawType thumbnail) {
881 updateState(CHANNEL_THUMBNAIL, createImageState(thumbnail));
885 public void updateFanart(RawType fanart) {
886 updateState(CHANNEL_FANART, createImageState(fanart));
890 public void updateAudioCodec(String codec) {
891 updateState(CHANNEL_AUDIO_CODEC, createStringState(codec));
895 public void updateAudioIndex(int index) {
896 updateState(CHANNEL_AUDIO_INDEX, new DecimalType(index));
900 public void updateAudioChannels(int channels) {
901 updateState(CHANNEL_AUDIO_CHANNELS, new DecimalType(channels));
905 public void updateAudioLanguage(String language) {
906 updateState(CHANNEL_AUDIO_LANGUAGE, createStringState(language));
910 public void updateAudioName(String name) {
911 updateState(CHANNEL_AUDIO_NAME, createStringState(name));
915 public void updateVideoCodec(String codec) {
916 updateState(CHANNEL_VIDEO_CODEC, createStringState(codec));
920 public void updateVideoIndex(int index) {
921 updateState(CHANNEL_VIDEO_INDEX, new DecimalType(index));
925 public void updateVideoHeight(int height) {
926 updateState(CHANNEL_VIDEO_HEIGHT, new DecimalType(height));
930 public void updateVideoWidth(int width) {
931 updateState(CHANNEL_VIDEO_WIDTH, new DecimalType(width));
935 public void updateSubtitleEnabled(boolean enabled) {
937 updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.ON);
939 updateState(CHANNEL_SUBTITLE_ENABLED, OnOffType.OFF);
944 public void updateSubtitleIndex(int index) {
945 updateState(CHANNEL_SUBTITLE_INDEX, new DecimalType(index));
949 public void updateSubtitleLanguage(String language) {
950 updateState(CHANNEL_SUBTITLE_LANGUAGE, createStringState(language));
954 public void updateSubtitleName(String name) {
955 updateState(CHANNEL_SUBTITLE_NAME, createStringState(name));
959 public void updateCurrentTime(long currentTime) {
960 updateState(CHANNEL_CURRENTTIME, createQuantityState(currentTime, SmartHomeUnits.SECOND));
964 public void updateCurrentTimePercentage(double currentTimePercentage) {
965 updateState(CHANNEL_CURRENTTIMEPERCENTAGE, createQuantityState(currentTimePercentage, SmartHomeUnits.PERCENT));
969 public void updateDuration(long duration) {
970 updateState(CHANNEL_DURATION, createQuantityState(duration, SmartHomeUnits.SECOND));
974 public void updateCurrentProfile(String profile) {
975 updateState(profileChannelUID, new StringType(profile));
979 public void updateSystemProperties(KodiSystemProperties systemProperties) {
980 if (systemProperties != null) {
981 List<CommandOption> options = new ArrayList<>();
982 if (systemProperties.canHibernate()) {
983 options.add(new CommandOption(SYSTEM_COMMAND_HIBERNATE, SYSTEM_COMMAND_HIBERNATE));
985 if (systemProperties.canReboot()) {
986 options.add(new CommandOption(SYSTEM_COMMAND_REBOOT, SYSTEM_COMMAND_REBOOT));
988 if (systemProperties.canShutdown()) {
989 options.add(new CommandOption(SYSTEM_COMMAND_SHUTDOWN, SYSTEM_COMMAND_SHUTDOWN));
991 if (systemProperties.canSuspend()) {
992 options.add(new CommandOption(SYSTEM_COMMAND_SUSPEND, SYSTEM_COMMAND_SUSPEND));
994 if (systemProperties.canQuit()) {
995 options.add(new CommandOption(SYSTEM_COMMAND_QUIT, SYSTEM_COMMAND_QUIT));
997 commandDescriptionProvider.setCommandOptions(new ChannelUID(getThing().getUID(), CHANNEL_SYSTEMCOMMAND),
1003 * Wrap the given String in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the String is empty.
1005 private State createStringState(String string) {
1006 if (string == null || string.isEmpty()) {
1007 return UnDefType.UNDEF;
1009 return new StringType(string);
1014 * Wrap the given list of Strings in a new {@link StringType} or returns {@link UnDefType#UNDEF} if the list of
1017 private State createStringListState(List<String> list) {
1018 if (list == null || list.isEmpty()) {
1019 return UnDefType.UNDEF;
1021 return createStringState(list.stream().collect(Collectors.joining(", ")));
1026 * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
1028 private State createImageState(@Nullable RawType image) {
1029 if (image == null) {
1030 return UnDefType.UNDEF;
1036 private State createQuantityState(Number value, Unit<?> unit) {
1037 return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit);