]> git.basschouten.com Git - openhab-addons.git/blob
6497261098fc7ba29e95d23ec534a7264ef09120
[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             Application app;
132             try {
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());
137                 return;
138             }
139
140             if (app != null) {
141                 closeApp(app.id);
142             }
143         }
144     }
145
146     private void handlePlayUri(Command command) {
147         if (command instanceof StringType) {
148             playMedia(null, command.toString(), null);
149         }
150     }
151
152     private void handleControl(final Command command) {
153         try {
154             Application app = chromeCast.getRunningApp();
155             statusUpdater.updateStatus(ThingStatus.ONLINE);
156             if (app == null) {
157                 logger.debug("{} command ignored because media player app is not running", command);
158                 return;
159             }
160
161             if (command instanceof PlayPauseType) {
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);
166                     return;
167                 }
168
169                 final PlayPauseType playPause = (PlayPauseType) command;
170                 if (playPause == PlayPauseType.PLAY) {
171                     chromeCast.play();
172                 } else if (playPause == PlayPauseType.PAUSE
173                         && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
174                     chromeCast.pause();
175                 } else {
176                     logger.info("{} command not supported by current media", command);
177                 }
178             }
179
180             if (command instanceof NextPreviousType) {
181                 // Next is implemented by seeking to the end of the current media
182                 if (command == NextPreviousType.NEXT) {
183                     Double duration = statusUpdater.getLastDuration();
184                     if (duration != null) {
185                         chromeCast.seek(duration.doubleValue() - 5);
186                     } else {
187                         logger.info("{} command failed - unknown media duration", command);
188                     }
189                 } else {
190                     logger.info("{} command not yet implemented", command);
191                     return;
192                 }
193             }
194
195         } catch (final IOException e) {
196             logger.debug("{} command failed: {}", command, e.getMessage());
197             statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
198         }
199     }
200
201     public void handleVolume(final Command command) {
202         if (command instanceof PercentType) {
203             setVolumeInternal((PercentType) command);
204         } else if (command == IncreaseDecreaseType.INCREASE) {
205             setVolumeInternal(new PercentType(
206                     Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
207         } else if (command == IncreaseDecreaseType.DECREASE) {
208             setVolumeInternal(new PercentType(
209                     Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
210         }
211     }
212
213     private void setVolumeInternal(PercentType volume) {
214         try {
215             chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
216             statusUpdater.updateStatus(ThingStatus.ONLINE);
217         } catch (final IOException ex) {
218             logger.debug("Set volume failed: {}", ex.getMessage());
219             statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
220         }
221     }
222
223     private void handleMute(final Command command) {
224         if (command instanceof OnOffType) {
225             final boolean mute = command == OnOffType.ON;
226             try {
227                 chromeCast.setMuted(mute);
228                 statusUpdater.updateStatus(ThingStatus.ONLINE);
229             } catch (final IOException ex) {
230                 logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
231                 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
232             }
233         }
234     }
235
236     public void startApp(@Nullable String appId) {
237         if (appId == null) {
238             return;
239         }
240         try {
241             if (chromeCast.isAppAvailable(appId)) {
242                 if (!chromeCast.isAppRunning(appId)) {
243                     final Application app = chromeCast.launchApp(appId);
244                     statusUpdater.setAppSessionId(app.sessionId);
245                     logger.debug("Application launched: {}", appId);
246                 }
247             } else {
248                 logger.warn("Failed starting app, app probably not installed. Appid: {}", appId);
249             }
250             statusUpdater.updateStatus(ThingStatus.ONLINE);
251         } catch (final IOException e) {
252             logger.warn("Failed starting app: {}. Message: {}", appId, e.getMessage());
253         }
254     }
255
256     public void closeApp(@Nullable String appId) {
257         if (appId == null) {
258             return;
259         }
260
261         try {
262             if (chromeCast.isAppRunning(appId)) {
263                 Application app = chromeCast.getRunningApp();
264                 if (app.id.equals(appId)) {
265                     chromeCast.stopApp();
266                     logger.debug("Application closed: {}", appId);
267                 }
268             }
269         } catch (final IOException e) {
270             logger.debug("Failed stopping app: {} with message: {}", appId, e.getMessage());
271         }
272     }
273
274     public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
275         startApp(MEDIA_PLAYER);
276         try {
277             if (url != null && chromeCast.isAppRunning(MEDIA_PLAYER)) {
278                 // If the current track is paused, launching a new request results in nothing happening, therefore
279                 // resume current track.
280                 MediaStatus ms = chromeCast.getMediaStatus();
281                 if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
282                     logger.debug("Current stream paused, resuming");
283                     chromeCast.play();
284                 } else {
285                     chromeCast.load(title, null, url, mimeType);
286                 }
287             } else {
288                 logger.warn("Missing media player app - cannot process media.");
289             }
290             statusUpdater.updateStatus(ThingStatus.ONLINE);
291         } catch (final IOException e) {
292             if ("Unable to load media".equals(e.getMessage())) {
293                 logger.warn("Unable to load media: {}", url);
294             } else {
295                 logger.debug("Failed playing media: {}", e.getMessage());
296                 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR,
297                         "IOException while trying to play media: " + e.getMessage());
298             }
299         }
300     }
301 }