2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.chromecast.internal;
15 import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
16 import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
18 import java.io.IOException;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.core.library.types.IncreaseDecreaseType;
23 import org.openhab.core.library.types.NextPreviousType;
24 import org.openhab.core.library.types.OnOffType;
25 import org.openhab.core.library.types.PercentType;
26 import org.openhab.core.library.types.PlayPauseType;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.ThingStatus;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.RefreshType;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 import su.litvak.chromecast.api.v2.Application;
36 import su.litvak.chromecast.api.v2.ChromeCast;
37 import su.litvak.chromecast.api.v2.MediaStatus;
38 import su.litvak.chromecast.api.v2.Status;
41 * This sends the various commands to the Chromecast.
43 * @author Jason Holmes - Initial contribution
46 public class ChromecastCommander {
47 private final Logger logger = LoggerFactory.getLogger(ChromecastCommander.class);
49 private final ChromeCast chromeCast;
50 private final ChromecastScheduler scheduler;
51 private final ChromecastStatusUpdater statusUpdater;
53 private static final int VOLUMESTEP = 10;
55 public ChromecastCommander(ChromeCast chromeCast, ChromecastScheduler scheduler,
56 ChromecastStatusUpdater statusUpdater) {
57 this.chromeCast = chromeCast;
58 this.scheduler = scheduler;
59 this.statusUpdater = statusUpdater;
62 public void handleCommand(final ChannelUID channelUID, final Command command) {
63 if (command instanceof RefreshType) {
64 scheduler.scheduleRefresh();
68 switch (channelUID.getId()) {
70 handleControl(command);
76 handleVolume(command);
81 case CHANNEL_PLAY_URI:
82 handlePlayUri(command);
85 logger.debug("Received command {} for unknown channel: {}", command, channelUID);
90 public void handleRefresh() {
91 if (!chromeCast.isConnected()) {
92 scheduler.cancelRefresh();
93 scheduler.scheduleConnect();
99 status = chromeCast.getStatus();
100 statusUpdater.processStatusUpdate(status);
102 if (status == null) {
103 scheduler.cancelRefresh();
105 } catch (IOException ex) {
106 logger.debug("Failed to request status: {}", ex.getMessage());
107 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
108 scheduler.cancelRefresh();
113 if (status != null && status.getRunningApp() != null) {
114 MediaStatus mediaStatus = chromeCast.getMediaStatus();
115 statusUpdater.updateMediaStatus(mediaStatus);
117 if (mediaStatus != null && mediaStatus.playerState == MediaStatus.PlayerState.IDLE
118 && mediaStatus.idleReason != null
119 && mediaStatus.idleReason != MediaStatus.IdleReason.INTERRUPTED) {
120 stopMediaPlayerApp();
123 } catch (IOException ex) {
124 logger.debug("Failed to request media status with a running app: {}", ex.getMessage());
125 // We were just able to request status, so let's not put the device OFFLINE.
129 private void handlePlayUri(Command command) {
130 if (command instanceof StringType) {
131 playMedia(null, command.toString(), null);
135 private void handleControl(final Command command) {
137 Application app = chromeCast.getRunningApp();
138 statusUpdater.updateStatus(ThingStatus.ONLINE);
140 logger.debug("{} command ignored because media player app is not running", command);
144 if (command instanceof PlayPauseType) {
145 MediaStatus mediaStatus = chromeCast.getMediaStatus();
146 logger.debug("mediaStatus {}", mediaStatus);
147 if (mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.IDLE) {
148 logger.debug("{} command ignored because media is not loaded", command);
152 final PlayPauseType playPause = (PlayPauseType) command;
153 if (playPause == PlayPauseType.PLAY) {
155 } else if (playPause == PlayPauseType.PAUSE
156 && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
159 logger.info("{} command not supported by current media", command);
163 if (command instanceof NextPreviousType) {
164 // Next is implemented by seeking to the end of the current media
165 if (command == NextPreviousType.NEXT) {
167 Double duration = statusUpdater.getLastDuration();
168 if (duration != null) {
169 chromeCast.seek(duration.doubleValue() - 5);
171 logger.info("{} command failed - unknown media duration", command);
174 logger.info("{} command not yet implemented", command);
179 } catch (final IOException e) {
180 logger.debug("{} command failed: {}", command, e.getMessage());
181 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
185 public void handleStop(final Command command) {
186 if (command == OnOffType.ON) {
188 chromeCast.stopApp();
189 statusUpdater.updateStatus(ThingStatus.ONLINE);
190 } catch (final IOException ex) {
191 logger.debug("{} command failed: {}", command, ex.getMessage());
192 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
197 public void handleVolume(final Command command) {
198 if (command instanceof PercentType) {
199 setVolumeInternal((PercentType) command);
200 } else if (command == IncreaseDecreaseType.INCREASE) {
201 setVolumeInternal(new PercentType(
202 Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
203 } else if (command == IncreaseDecreaseType.DECREASE) {
204 setVolumeInternal(new PercentType(
205 Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
209 private void setVolumeInternal(PercentType volume) {
211 chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
212 statusUpdater.updateStatus(ThingStatus.ONLINE);
213 } catch (final IOException ex) {
214 logger.debug("Set volume failed: {}", ex.getMessage());
215 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
219 private void handleMute(final Command command) {
220 if (command instanceof OnOffType) {
221 final boolean mute = command == OnOffType.ON;
223 chromeCast.setMuted(mute);
224 statusUpdater.updateStatus(ThingStatus.ONLINE);
225 } catch (final IOException ex) {
226 logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
227 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
232 public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
234 if (chromeCast.isAppAvailable(MEDIA_PLAYER)) {
235 if (!chromeCast.isAppRunning(MEDIA_PLAYER)) {
236 final Application app = chromeCast.launchApp(MEDIA_PLAYER);
237 statusUpdater.setAppSessionId(app.sessionId);
238 logger.debug("Application launched: {}", app);
241 // If the current track is paused, launching a new request results in nothing happening, therefore
242 // resume current track.
243 MediaStatus ms = chromeCast.getMediaStatus();
244 if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
245 logger.debug("Current stream paused, resuming");
248 chromeCast.load(title, null, url, mimeType);
252 logger.warn("Missing media player app - cannot process media.");
254 statusUpdater.updateStatus(ThingStatus.ONLINE);
255 } catch (final IOException e) {
256 logger.debug("Failed playing media: {}", e.getMessage());
257 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
261 private void stopMediaPlayerApp() {
263 Application app = chromeCast.getRunningApp();
264 if (app.id.equals(MEDIA_PLAYER) && app.sessionId.equals(statusUpdater.getAppSessionId())) {
265 chromeCast.stopApp();
266 logger.debug("Media player app stopped");
268 } catch (final IOException e) {
269 logger.debug("Failed stopping media player app", e);