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) {
133 app = chromeCast.getRunningApp();
134 } catch (final IOException e) {
135 logger.info("{} command failed: {}", command, e.getMessage());
136 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
146 private void handlePlayUri(Command command) {
147 if (command instanceof StringType) {
148 playMedia(null, command.toString(), null);
152 private void handleControl(final Command command) {
154 Application app = chromeCast.getRunningApp();
155 statusUpdater.updateStatus(ThingStatus.ONLINE);
157 logger.debug("{} command ignored because media player app is not running", command);
161 if (command instanceof PlayPauseType playPauseCommand) {
162 MediaStatus mediaStatus = chromeCast.getMediaStatus();
163 logger.debug("mediaStatus {}", mediaStatus);
164 if (mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.IDLE) {
165 logger.debug("{} command ignored because media is not loaded", command);
168 if (playPauseCommand == PlayPauseType.PLAY) {
170 } else if (playPauseCommand == PlayPauseType.PAUSE
171 && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
174 logger.info("{} command not supported by current media", command);
178 if (command instanceof NextPreviousType) {
179 // Next is implemented by seeking to the end of the current media
180 if (command == NextPreviousType.NEXT) {
181 Double duration = statusUpdater.getLastDuration();
182 if (duration != null) {
183 chromeCast.seek(duration.doubleValue() - 5);
185 logger.info("{} command failed - unknown media duration", command);
188 logger.info("{} command not yet implemented", command);
193 } catch (final IOException e) {
194 logger.debug("{} command failed: {}", command, e.getMessage());
195 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
199 public void handleVolume(final Command command) {
200 if (command instanceof PercentType percentCommand) {
201 setVolumeInternal(percentCommand);
202 } else if (command == IncreaseDecreaseType.INCREASE) {
203 setVolumeInternal(new PercentType(
204 Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
205 } else if (command == IncreaseDecreaseType.DECREASE) {
206 setVolumeInternal(new PercentType(
207 Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
211 private void setVolumeInternal(PercentType volume) {
213 chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
214 statusUpdater.updateStatus(ThingStatus.ONLINE);
215 } catch (final IOException ex) {
216 logger.debug("Set volume failed: {}", ex.getMessage());
217 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
221 private void handleMute(final Command command) {
222 if (command instanceof OnOffType) {
223 final boolean mute = command == OnOffType.ON;
225 chromeCast.setMuted(mute);
226 statusUpdater.updateStatus(ThingStatus.ONLINE);
227 } catch (final IOException ex) {
228 logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
229 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
234 public void startApp(@Nullable String appId) {
239 if (chromeCast.isAppAvailable(appId)) {
240 if (!chromeCast.isAppRunning(appId)) {
241 final Application app = chromeCast.launchApp(appId);
242 statusUpdater.setAppSessionId(app.sessionId);
243 logger.debug("Application launched: {}", appId);
246 logger.warn("Failed starting app, app probably not installed. Appid: {}", appId);
248 statusUpdater.updateStatus(ThingStatus.ONLINE);
249 } catch (final IOException e) {
250 logger.warn("Failed starting app: {}. Message: {}", appId, e.getMessage());
254 public void closeApp(@Nullable String appId) {
260 if (chromeCast.isAppRunning(appId)) {
261 Application app = chromeCast.getRunningApp();
262 if (app.id.equals(appId)) {
263 chromeCast.stopApp();
264 logger.debug("Application closed: {}", appId);
267 } catch (final IOException e) {
268 logger.debug("Failed stopping app: {} with message: {}", appId, e.getMessage());
272 public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
273 startApp(MEDIA_PLAYER);
275 if (url != null && chromeCast.isAppRunning(MEDIA_PLAYER)) {
276 // If the current track is paused, launching a new request results in nothing happening, therefore
277 // resume current track.
278 MediaStatus ms = chromeCast.getMediaStatus();
279 if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
280 logger.debug("Current stream paused, resuming");
283 chromeCast.load(title, null, url, mimeType);
286 logger.warn("Missing media player app - cannot process media.");
288 statusUpdater.updateStatus(ThingStatus.ONLINE);
289 } catch (final IOException e) {
290 if ("Unable to load media".equals(e.getMessage())) {
291 logger.warn("Unable to load media: {}", url);
293 logger.debug("Failed playing media: {}", e.getMessage());
294 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR,
295 "IOException while trying to play media: " + e.getMessage());