]> git.basschouten.com Git - openhab-addons.git/blob
a3d5206079721145cec5373c7e3b63bd9fed8e41
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.chromecast.internal;
14
15 import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
16 import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
17
18 import java.io.IOException;
19
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;
34
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;
39
40 /**
41  * This sends the various commands to the Chromecast.
42  *
43  * @author Jason Holmes - Initial contribution
44  */
45 @NonNullByDefault
46 public class ChromecastCommander {
47     private final Logger logger = LoggerFactory.getLogger(ChromecastCommander.class);
48
49     private final ChromeCast chromeCast;
50     private final ChromecastScheduler scheduler;
51     private final ChromecastStatusUpdater statusUpdater;
52
53     private static final int VOLUMESTEP = 10;
54
55     public ChromecastCommander(ChromeCast chromeCast, ChromecastScheduler scheduler,
56             ChromecastStatusUpdater statusUpdater) {
57         this.chromeCast = chromeCast;
58         this.scheduler = scheduler;
59         this.statusUpdater = statusUpdater;
60     }
61
62     public void handleCommand(final ChannelUID channelUID, final Command command) {
63         if (command instanceof RefreshType) {
64             scheduler.scheduleRefresh();
65             return;
66         }
67
68         switch (channelUID.getId()) {
69             case CHANNEL_CONTROL:
70                 handleControl(command);
71                 break;
72             case CHANNEL_STOP:
73                 handleCloseApp(command);
74                 break;
75             case CHANNEL_VOLUME:
76                 handleVolume(command);
77                 break;
78             case CHANNEL_MUTE:
79                 handleMute(command);
80                 break;
81             case CHANNEL_PLAY_URI:
82                 handlePlayUri(command);
83                 break;
84             default:
85                 logger.debug("Received command {} for unknown channel: {}", command, channelUID);
86                 break;
87         }
88     }
89
90     public void handleRefresh() {
91         if (!chromeCast.isConnected()) {
92             scheduler.cancelRefresh();
93             scheduler.scheduleConnect();
94             return;
95         }
96
97         Status status;
98         try {
99             status = chromeCast.getStatus();
100             statusUpdater.processStatusUpdate(status);
101
102             if (status == null) {
103                 scheduler.cancelRefresh();
104             }
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();
109             return;
110         }
111
112         try {
113             if (status != null && status.getRunningApp() != null) {
114                 MediaStatus mediaStatus = chromeCast.getMediaStatus();
115                 statusUpdater.updateMediaStatus(mediaStatus);
116
117                 if (mediaStatus != null && mediaStatus.playerState == MediaStatus.PlayerState.IDLE
118                         && mediaStatus.idleReason != null
119                         && mediaStatus.idleReason != MediaStatus.IdleReason.INTERRUPTED) {
120                     closeApp(MEDIA_PLAYER);
121                 }
122             }
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.
126         }
127     }
128
129     public void handleCloseApp(final Command command) {
130         if (command == OnOffType.ON) {
131             closeApp(MEDIA_PLAYER);
132         }
133     }
134
135     private void handlePlayUri(Command command) {
136         if (command instanceof StringType) {
137             playMedia(null, command.toString(), null);
138         }
139     }
140
141     private void handleControl(final Command command) {
142         try {
143             Application app = chromeCast.getRunningApp();
144             statusUpdater.updateStatus(ThingStatus.ONLINE);
145             if (app == null) {
146                 logger.debug("{} command ignored because media player app is not running", command);
147                 return;
148             }
149
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);
155                     return;
156                 }
157
158                 final PlayPauseType playPause = (PlayPauseType) command;
159                 if (playPause == PlayPauseType.PLAY) {
160                     chromeCast.play();
161                 } else if (playPause == PlayPauseType.PAUSE
162                         && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
163                     chromeCast.pause();
164                 } else {
165                     logger.info("{} command not supported by current media", command);
166                 }
167             }
168
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);
175                     } else {
176                         logger.info("{} command failed - unknown media duration", command);
177                     }
178                 } else {
179                     logger.info("{} command not yet implemented", command);
180                     return;
181                 }
182             }
183
184         } catch (final IOException e) {
185             logger.debug("{} command failed: {}", command, e.getMessage());
186             statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
187         }
188     }
189
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())));
199         }
200     }
201
202     private void setVolumeInternal(PercentType volume) {
203         try {
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());
209         }
210     }
211
212     private void handleMute(final Command command) {
213         if (command instanceof OnOffType) {
214             final boolean mute = command == OnOffType.ON;
215             try {
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());
221             }
222         }
223     }
224
225     public void startApp(@Nullable String appId) {
226         if (appId == null) {
227             return;
228         }
229         try {
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);
235                 }
236             } else {
237                 logger.warn("Failed starting app, app probably not installed. Appid: {}", appId);
238             }
239             statusUpdater.updateStatus(ThingStatus.ONLINE);
240         } catch (final IOException e) {
241             logger.warn("Failed starting app: {}. Message: {}", appId, e.getMessage());
242         }
243     }
244
245     public void closeApp(@Nullable String appId) {
246         if (appId == null) {
247             return;
248         }
249
250         try {
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);
256                 }
257             }
258         } catch (final IOException e) {
259             logger.debug("Failed stopping media player app: {} with message: {}", appId, e.getMessage());
260         }
261     }
262
263     public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
264         startApp(MEDIA_PLAYER);
265         try {
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");
272                     chromeCast.play();
273                 } else {
274                     chromeCast.load(title, null, url, mimeType);
275                 }
276             } else {
277                 logger.warn("Missing media player app - cannot process media.");
278             }
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);
283             } else {
284                 logger.debug("Failed playing media: {}", e.getMessage());
285                 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR,
286                         "IOException while trying to play media: " + e.getMessage());
287             }
288         }
289     }
290 }