]> git.basschouten.com Git - openhab-addons.git/blob
b68ea53779e2e20586396ebc8b4d379a7cfbeec0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.handler;
14
15 import java.io.IOException;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Locale;
19 import java.util.Set;
20 import java.util.stream.Collectors;
21 import java.util.stream.Stream;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.chromecast.internal.ChromecastAudioSink;
26 import org.openhab.binding.chromecast.internal.ChromecastCommander;
27 import org.openhab.binding.chromecast.internal.ChromecastEventReceiver;
28 import org.openhab.binding.chromecast.internal.ChromecastScheduler;
29 import org.openhab.binding.chromecast.internal.ChromecastStatusUpdater;
30 import org.openhab.binding.chromecast.internal.action.ChromecastActions;
31 import org.openhab.binding.chromecast.internal.config.ChromecastConfig;
32 import org.openhab.core.audio.AudioFormat;
33 import org.openhab.core.audio.AudioHTTPServer;
34 import org.openhab.core.audio.AudioSink;
35 import org.openhab.core.audio.AudioStream;
36 import org.openhab.core.audio.UnsupportedAudioFormatException;
37 import org.openhab.core.audio.UnsupportedAudioStreamException;
38 import org.openhab.core.library.types.PercentType;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.State;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import su.litvak.chromecast.api.v2.ChromeCast;
51
52 /**
53  * The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
54  * furthermore implements {@link AudioSink} support.
55  *
56  * @author Markus Rathgeb, Kai Kreuzer - Initial contribution
57  * @author Daniel Walters - Online status fix, handle playuri channel and refactor play media code
58  * @author Jason Holmes - Media Status. Refactor the monolith into separate classes.
59  * @author Scott Hanson - Added Actions.
60  */
61 @NonNullByDefault
62 public class ChromecastHandler extends BaseThingHandler implements AudioSink {
63
64     private static final Set<AudioFormat> SUPPORTED_FORMATS = Collections
65             .unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
66     private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Collections.singleton(AudioStream.class);
67
68     private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
69     private final AudioHTTPServer audioHTTPServer;
70     private final @Nullable String callbackUrl;
71
72     /**
73      * The actual implementation. A new one is created each time #initialize is called.
74      */
75     private @Nullable Coordinator coordinator;
76
77     /**
78      * Constructor.
79      *
80      * @param thing the thing the coordinator should be created for
81      * @param audioHTTPServer server for hosting audio streams
82      * @param callbackUrl url to be used to tell the Chromecast which host to call for audio urls
83      */
84     public ChromecastHandler(final Thing thing, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
85         super(thing);
86         this.audioHTTPServer = audioHTTPServer;
87         this.callbackUrl = callbackUrl;
88     }
89
90     @Override
91     public void initialize() {
92         ChromecastConfig config = getConfigAs(ChromecastConfig.class);
93
94         final String ipAddress = config.ipAddress;
95         if (ipAddress == null || ipAddress.isEmpty()) {
96             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
97                     "Cannot connect to Chromecast. IP address is not valid or missing.");
98             return;
99         }
100
101         Coordinator localCoordinator = coordinator;
102         if (localCoordinator != null && (!localCoordinator.chromeCast.getAddress().equals(ipAddress)
103                 || (localCoordinator.chromeCast.getPort() != config.port))) {
104             localCoordinator.destroy();
105             localCoordinator = coordinator = null;
106         }
107
108         if (localCoordinator == null) {
109             ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
110             localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate, audioHTTPServer,
111                     callbackUrl);
112             localCoordinator.initialize();
113             coordinator = localCoordinator;
114         }
115     }
116
117     @Override
118     public void dispose() {
119         Coordinator localCoordinator = coordinator;
120         if (localCoordinator != null) {
121             localCoordinator.destroy();
122             coordinator = null;
123         }
124     }
125
126     @Override
127     public void handleCommand(final ChannelUID channelUID, final Command command) {
128         Coordinator localCoordinator = coordinator;
129         if (localCoordinator != null) {
130             localCoordinator.commander.handleCommand(channelUID, command);
131         } else {
132             logger.debug("Cannot handle command. No coordinator has been initialized");
133         }
134     }
135
136     @Override // Just exposing this for ChromecastStatusUpdater.
137     public void updateState(String channelId, State state) {
138         super.updateState(channelId, state);
139     }
140
141     @Override // Just exposing this for ChromecastStatusUpdater.
142     public void updateState(ChannelUID channelUID, State state) {
143         super.updateState(channelUID, state);
144     }
145
146     @Override // Just exposing this for ChromecastStatusUpdater.
147     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
148         super.updateStatus(status, statusDetail, description);
149     }
150
151     @Override // Just exposing this for ChromecastStatusUpdater.
152     public boolean isLinked(String channelId) {
153         return super.isLinked(channelId);
154     }
155
156     @Override // Just exposing this for ChromecastStatusUpdater.
157     public boolean isLinked(ChannelUID channelUID) {
158         return super.isLinked(channelUID);
159     }
160
161     @Override
162     public String getId() {
163         return thing.getUID().toString();
164     }
165
166     @Override
167     public @Nullable String getLabel(@Nullable Locale locale) {
168         return thing.getLabel();
169     }
170
171     @Override
172     public Set<AudioFormat> getSupportedFormats() {
173         return SUPPORTED_FORMATS;
174     }
175
176     @Override
177     public Set<Class<? extends AudioStream>> getSupportedStreams() {
178         return SUPPORTED_STREAMS;
179     }
180
181     @Override
182     public void process(@Nullable AudioStream audioStream)
183             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
184         Coordinator localCoordinator = coordinator;
185         if (localCoordinator != null) {
186             localCoordinator.audioSink.process(audioStream);
187         } else {
188             logger.debug("Cannot process audioStream. No coordinator has been initialized.");
189         }
190     }
191
192     @Override
193     public PercentType getVolume() throws IOException {
194         Coordinator localCoordinator = coordinator;
195         if (localCoordinator != null) {
196             return localCoordinator.statusUpdater.getVolume();
197         } else {
198             throw new IOException("Cannot get volume. No coordinator has been initialized.");
199         }
200     }
201
202     @Override
203     public void setVolume(PercentType percentType) throws IOException {
204         Coordinator localCoordinator = coordinator;
205         if (localCoordinator != null) {
206             localCoordinator.commander.handleVolume(percentType);
207         } else {
208             throw new IOException("Cannot set volume. No coordinator has been initialized.");
209         }
210     }
211
212     @Override
213     public Collection<Class<? extends ThingHandlerService>> getServices() {
214         return Collections.singletonList(ChromecastActions.class);
215     }
216
217     public boolean playURL(String url, @Nullable String mediaType) {
218         Coordinator localCoordinator = coordinator;
219         if (localCoordinator != null) {
220             localCoordinator.commander.playMedia(null, url, mediaType);
221             return true;
222         }
223         return false;
224     }
225
226     private static class Coordinator {
227         private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
228
229         private static final long CONNECT_DELAY = 10;
230
231         private final ChromeCast chromeCast;
232         private final ChromecastAudioSink audioSink;
233         private final ChromecastCommander commander;
234         private final ChromecastEventReceiver eventReceiver;
235         private final ChromecastStatusUpdater statusUpdater;
236         private final ChromecastScheduler scheduler;
237
238         private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate,
239                 AudioHTTPServer audioHttpServer, @Nullable String callbackURL) {
240             this.chromeCast = chromeCast;
241
242             this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
243                     this::refresh);
244             this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
245
246             this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
247             this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
248             this.audioSink = new ChromecastAudioSink(commander, audioHttpServer, callbackURL);
249         }
250
251         void initialize() {
252             chromeCast.registerListener(eventReceiver);
253             chromeCast.registerConnectionListener(eventReceiver);
254
255             this.connect();
256         }
257
258         void destroy() {
259             chromeCast.unregisterConnectionListener(eventReceiver);
260             chromeCast.unregisterListener(eventReceiver);
261
262             try {
263                 scheduler.destroy();
264                 chromeCast.disconnect();
265             } catch (final IOException ex) {
266                 logger.debug("Disconnect failed: {}", ex.getMessage());
267             }
268         }
269
270         private void connect() {
271             try {
272                 chromeCast.connect();
273                 statusUpdater.updateMediaStatus(null);
274                 statusUpdater.updateStatus(ThingStatus.ONLINE);
275             } catch (final Exception e) {
276                 statusUpdater.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
277                         e.getMessage());
278                 scheduler.scheduleConnect();
279             }
280         }
281
282         private void refresh() {
283             commander.handleRefresh();
284         }
285     }
286 }