]> git.basschouten.com Git - openhab-addons.git/blob
411e8e953670ee92c511f67ebefcfafcfca3ee80
[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 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);
166                     return;
167                 }
168                 if (playPauseCommand == PlayPauseType.PLAY) {
169                     chromeCast.play();
170                 } else if (playPauseCommand == PlayPauseType.PAUSE
171                         && ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
172                     chromeCast.pause();
173                 } else {
174                     logger.info("{} command not supported by current media", command);
175                 }
176             }
177
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);
184                     } else {
185                         logger.info("{} command failed - unknown media duration", command);
186                     }
187                 } else {
188                     logger.info("{} command not yet implemented", command);
189                     return;
190                 }
191             }
192
193         } catch (final IOException e) {
194             logger.debug("{} command failed: {}", command, e.getMessage());
195             statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
196         }
197     }
198
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())));
208         }
209     }
210
211     private void setVolumeInternal(PercentType volume) {
212         try {
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());
218         }
219     }
220
221     private void handleMute(final Command command) {
222         if (command instanceof OnOffType) {
223             final boolean mute = command == OnOffType.ON;
224             try {
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());
230             }
231         }
232     }
233
234     public void startApp(@Nullable String appId) {
235         if (appId == null) {
236             return;
237         }
238         try {
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);
244                 }
245             } else {
246                 logger.warn("Failed starting app, app probably not installed. Appid: {}", appId);
247             }
248             statusUpdater.updateStatus(ThingStatus.ONLINE);
249         } catch (final IOException e) {
250             logger.warn("Failed starting app: {}. Message: {}", appId, e.getMessage());
251         }
252     }
253
254     public void closeApp(@Nullable String appId) {
255         if (appId == null) {
256             return;
257         }
258
259         try {
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);
265                 }
266             }
267         } catch (final IOException e) {
268             logger.debug("Failed stopping app: {} with message: {}", appId, e.getMessage());
269         }
270     }
271
272     public void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
273         startApp(MEDIA_PLAYER);
274         try {
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");
281                     chromeCast.play();
282                 } else {
283                     chromeCast.load(title, null, url, mimeType);
284                 }
285             } else {
286                 logger.warn("Missing media player app - cannot process media.");
287             }
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);
292             } else {
293                 logger.debug("Failed playing media: {}", e.getMessage());
294                 statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR,
295                         "IOException while trying to play media: " + e.getMessage());
296             }
297         }
298     }
299 }