2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.chromecast.internal.handler;
15 import java.io.IOException;
16 import java.util.Collection;
17 import java.util.Collections;
18 import java.util.Locale;
20 import java.util.stream.Collectors;
21 import java.util.stream.Stream;
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;
50 import su.litvak.chromecast.api.v2.ChromeCast;
53 * The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
54 * furthermore implements {@link AudioSink} support.
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.
62 public class ChromecastHandler extends BaseThingHandler implements AudioSink {
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);
68 private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
69 private final AudioHTTPServer audioHTTPServer;
70 private final @Nullable String callbackUrl;
73 * The actual implementation. A new one is created each time #initialize is called.
75 private @Nullable Coordinator coordinator;
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
84 public ChromecastHandler(final Thing thing, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
86 this.audioHTTPServer = audioHTTPServer;
87 this.callbackUrl = callbackUrl;
91 public void initialize() {
92 ChromecastConfig config = getConfigAs(ChromecastConfig.class);
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.");
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;
108 if (localCoordinator == null) {
109 ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
110 localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate, audioHTTPServer,
112 localCoordinator.initialize();
113 coordinator = localCoordinator;
118 public void dispose() {
119 Coordinator localCoordinator = coordinator;
120 if (localCoordinator != null) {
121 localCoordinator.destroy();
127 public void handleCommand(final ChannelUID channelUID, final Command command) {
128 Coordinator localCoordinator = coordinator;
129 if (localCoordinator != null) {
130 localCoordinator.commander.handleCommand(channelUID, command);
132 logger.debug("Cannot handle command. No coordinator has been initialized");
136 @Override // Just exposing this for ChromecastStatusUpdater.
137 public void updateState(String channelId, State state) {
138 super.updateState(channelId, state);
141 @Override // Just exposing this for ChromecastStatusUpdater.
142 public void updateState(ChannelUID channelUID, State state) {
143 super.updateState(channelUID, state);
146 @Override // Just exposing this for ChromecastStatusUpdater.
147 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
148 super.updateStatus(status, statusDetail, description);
151 @Override // Just exposing this for ChromecastStatusUpdater.
152 public boolean isLinked(String channelId) {
153 return super.isLinked(channelId);
156 @Override // Just exposing this for ChromecastStatusUpdater.
157 public boolean isLinked(ChannelUID channelUID) {
158 return super.isLinked(channelUID);
162 public String getId() {
163 return thing.getUID().toString();
167 public @Nullable String getLabel(@Nullable Locale locale) {
168 return thing.getLabel();
172 public Set<AudioFormat> getSupportedFormats() {
173 return SUPPORTED_FORMATS;
177 public Set<Class<? extends AudioStream>> getSupportedStreams() {
178 return SUPPORTED_STREAMS;
182 public void process(@Nullable AudioStream audioStream)
183 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
184 Coordinator localCoordinator = coordinator;
185 if (localCoordinator != null) {
186 localCoordinator.audioSink.process(audioStream);
188 logger.debug("Cannot process audioStream. No coordinator has been initialized.");
193 public PercentType getVolume() throws IOException {
194 Coordinator localCoordinator = coordinator;
195 if (localCoordinator != null) {
196 return localCoordinator.statusUpdater.getVolume();
198 throw new IOException("Cannot get volume. No coordinator has been initialized.");
203 public void setVolume(PercentType percentType) throws IOException {
204 Coordinator localCoordinator = coordinator;
205 if (localCoordinator != null) {
206 localCoordinator.commander.handleVolume(percentType);
208 throw new IOException("Cannot set volume. No coordinator has been initialized.");
213 public Collection<Class<? extends ThingHandlerService>> getServices() {
214 return Collections.singletonList(ChromecastActions.class);
217 public boolean playURL(String url, @Nullable String mediaType) {
218 Coordinator localCoordinator = coordinator;
219 if (localCoordinator != null) {
220 localCoordinator.commander.playMedia(null, url, mediaType);
226 private static class Coordinator {
227 private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
229 private static final long CONNECT_DELAY = 10;
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;
238 private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate,
239 AudioHTTPServer audioHttpServer, @Nullable String callbackURL) {
240 this.chromeCast = chromeCast;
242 this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
244 this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
246 this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
247 this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
248 this.audioSink = new ChromecastAudioSink(commander, audioHttpServer, callbackURL);
252 chromeCast.registerListener(eventReceiver);
253 chromeCast.registerConnectionListener(eventReceiver);
259 chromeCast.unregisterConnectionListener(eventReceiver);
260 chromeCast.unregisterListener(eventReceiver);
264 chromeCast.disconnect();
265 } catch (final IOException ex) {
266 logger.debug("Disconnect failed: {}", ex.getMessage());
270 private void connect() {
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,
278 scheduler.scheduleConnect();
282 private void refresh() {
283 commander.handleRefresh();