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.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);
73 handleCloseApp(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 closeApp(MEDIA_PLAYER);
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 public void handleCloseApp(final Command command) {
130 if (command == OnOffType.ON) {
131 closeApp(MEDIA_PLAYER);
135 private void handlePlayUri(Command command) {
136 if (command instanceof StringType) {
137 playMedia(null, command.toString(), null);
141 private void handleControl(final Command command) {
143 Application app = chromeCast.getRunningApp();
144 statusUpdater.updateStatus(ThingStatus.ONLINE);
146 logger.debug("{} command ignored because media player app is not running", command);
150 if (command instanceof PlayPauseType) {
151 MediaStatus mediaStatus = chromeCast.getMediaStatus();
152 logger.debug("mediaStatus {}", mediaStatus);
153 if (mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.IDLE) {
154 logger.debug("{} command ignored because media is not loaded", command);
158 final PlayPauseType playPause = (PlayPauseType) command;
159 if (playPause == PlayPauseType.PLAY) {
161 } else if (playPause == PlayPauseType.PAUSE
162 && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
165 logger.info("{} command not supported by current media", command);
169 if (command instanceof NextPreviousType) {
170 // Next is implemented by seeking to the end of the current media
171 if (command == NextPreviousType.NEXT) {
172 Double duration = statusUpdater.getLastDuration();
173 if (duration != null) {
174 chromeCast.seek(duration.doubleValue() - 5);
176 logger.info("{} command failed - unknown media duration", command);
179 logger.info("{} command not yet implemented", command);
184 } catch (final IOException e) {
185 logger.debug("{} command failed: {}", command, e.getMessage());
186 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
190 public void handleVolume(final Command command) {
191 if (command instanceof PercentType) {
192 setVolumeInternal((PercentType) command);
193 } else if (command == IncreaseDecreaseType.INCREASE) {
194 setVolumeInternal(new PercentType(
195 Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
196 } else if (command == IncreaseDecreaseType.DECREASE) {
197 setVolumeInternal(new PercentType(
198 Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
202 private void setVolumeInternal(PercentType volume) {
204 chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
205 statusUpdater.updateStatus(ThingStatus.ONLINE);
206 } catch (final IOException ex) {
207 logger.debug("Set volume failed: {}", ex.getMessage());
208 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
212 private void handleMute(final Command command) {
213 if (command instanceof OnOffType) {
214 final boolean mute = command == OnOffType.ON;
216 chromeCast.setMuted(mute);
217 statusUpdater.updateStatus(ThingStatus.ONLINE);
218 } catch (final IOException ex) {
219 logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
220 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
225 public void startApp(@Nullable String appId) {
230 if (chromeCast.isAppAvailable(appId)) {
231 if (!chromeCast.isAppRunning(appId)) {
232 final Application app = chromeCast.launchApp(appId);
233 statusUpdater.setAppSessionId(app.sessionId);
234 logger.debug("Application launched: {}", appId);
237 logger.warn("Failed starting app, app probably not installed. Appid: {}", appId);
239 statusUpdater.updateStatus(ThingStatus.ONLINE);
240 } catch (final IOException e) {
241 logger.warn("Failed starting app: {}. Message: {}", appId, e.getMessage());
245 public void closeApp(@Nullable String appId) {
251 if (chromeCast.isAppRunning(appId)) {
252 Application app = chromeCast.getRunningApp();
253 if (app.id.equals(appId) && app.sessionId.equals(statusUpdater.getAppSessionId())) {
254 chromeCast.stopApp();
255 logger.debug("Application closed: {}", appId);
258 } catch (final IOException e) {
259 logger.debug("Failed stopping media player app: {} with message: {}", appId, e.getMessage());
263 public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
264 startApp(MEDIA_PLAYER);
266 if (url != null && chromeCast.isAppRunning(MEDIA_PLAYER)) {
267 // If the current track is paused, launching a new request results in nothing happening, therefore
268 // resume current track.
269 MediaStatus ms = chromeCast.getMediaStatus();
270 if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
271 logger.debug("Current stream paused, resuming");
274 chromeCast.load(title, null, url, mimeType);
277 logger.warn("Missing media player app - cannot process media.");
279 statusUpdater.updateStatus(ThingStatus.ONLINE);
280 } catch (final IOException e) {
281 if ("Unable to load media".equals(e.getMessage())) {
282 logger.warn("Unable to load media: {}", url);
284 logger.debug("Failed playing media: {}", e.getMessage());
285 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR,
286 "IOException while trying to play media: " + e.getMessage());