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.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 if (command instanceof NextPreviousType) {
138 // I can't find a way to control next/previous from the API. The Google app doesn't seem to
139 // allow it either, so I suspect there isn't a way.
140 logger.info("{} command not yet implemented", command);
144 Application app = chromeCast.getRunningApp();
145 statusUpdater.updateStatus(ThingStatus.ONLINE);
147 logger.debug("{} command ignored because media player app is not running", command);
151 if (command instanceof PlayPauseType) {
152 MediaStatus mediaStatus = chromeCast.getMediaStatus();
153 logger.debug("mediaStatus {}", mediaStatus);
154 if (mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.IDLE) {
155 logger.debug("{} command ignored because media is not loaded", command);
159 final PlayPauseType playPause = (PlayPauseType) command;
160 if (playPause == PlayPauseType.PLAY) {
162 } else if (playPause == PlayPauseType.PAUSE
163 && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
166 logger.info("{} command not supported by current media", command);
169 } catch (final IOException e) {
170 logger.debug("{} command failed: {}", command, e.getMessage());
171 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
175 public void handleStop(final Command command) {
176 if (command == OnOffType.ON) {
178 chromeCast.stopApp();
179 statusUpdater.updateStatus(ThingStatus.ONLINE);
180 } catch (final IOException ex) {
181 logger.debug("{} command failed: {}", command, ex.getMessage());
182 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
187 public void handleVolume(final Command command) {
188 if (command instanceof PercentType) {
189 setVolumeInternal((PercentType) command);
190 } else if (command == IncreaseDecreaseType.INCREASE) {
191 setVolumeInternal(new PercentType(
192 Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
193 } else if (command == IncreaseDecreaseType.DECREASE) {
194 setVolumeInternal(new PercentType(
195 Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
199 private void setVolumeInternal(PercentType volume) {
201 chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
202 statusUpdater.updateStatus(ThingStatus.ONLINE);
203 } catch (final IOException ex) {
204 logger.debug("Set volume failed: {}", ex.getMessage());
205 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
209 private void handleMute(final Command command) {
210 if (command instanceof OnOffType) {
211 final boolean mute = command == OnOffType.ON;
213 chromeCast.setMuted(mute);
214 statusUpdater.updateStatus(ThingStatus.ONLINE);
215 } catch (final IOException ex) {
216 logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
217 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
222 public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
224 if (chromeCast.isAppAvailable(MEDIA_PLAYER)) {
225 if (!chromeCast.isAppRunning(MEDIA_PLAYER)) {
226 final Application app = chromeCast.launchApp(MEDIA_PLAYER);
227 statusUpdater.setAppSessionId(app.sessionId);
228 logger.debug("Application launched: {}", app);
231 // If the current track is paused, launching a new request results in nothing happening, therefore
232 // resume current track.
233 MediaStatus ms = chromeCast.getMediaStatus();
234 if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
235 logger.debug("Current stream paused, resuming");
238 chromeCast.load(title, null, url, mimeType);
242 logger.warn("Missing media player app - cannot process media.");
244 statusUpdater.updateStatus(ThingStatus.ONLINE);
245 } catch (final IOException e) {
246 logger.debug("Failed playing media: {}", e.getMessage());
247 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
251 private void stopMediaPlayerApp() {
253 Application app = chromeCast.getRunningApp();
254 if (app.id.equals(MEDIA_PLAYER) && app.sessionId.equals(statusUpdater.getAppSessionId())) {
255 chromeCast.stopApp();
256 logger.debug("Media player app stopped");
258 } catch (final IOException e) {
259 logger.debug("Failed stopping media player app", e);