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