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.Collections;
17 import java.util.Locale;
19 import java.util.stream.Collectors;
20 import java.util.stream.Stream;
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;
47 import su.litvak.chromecast.api.v2.ChromeCast;
50 * The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
51 * furthermore implements {@link AudioSink} support.
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.
58 public class ChromecastHandler extends BaseThingHandler implements AudioSink {
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);
64 private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
65 private final AudioHTTPServer audioHTTPServer;
66 private final @Nullable String callbackUrl;
69 * The actual implementation. A new one is created each time #initialize is called.
71 private @Nullable Coordinator coordinator;
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
80 public ChromecastHandler(final Thing thing, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
82 this.audioHTTPServer = audioHTTPServer;
83 this.callbackUrl = callbackUrl;
87 public void initialize() {
88 ChromecastConfig config = getConfigAs(ChromecastConfig.class);
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.");
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;
104 if (localCoordinator == null) {
105 ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
106 localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate, audioHTTPServer,
108 localCoordinator.initialize();
109 coordinator = localCoordinator;
114 public void dispose() {
115 Coordinator localCoordinator = coordinator;
116 if (localCoordinator != null) {
117 localCoordinator.destroy();
123 public void handleCommand(final ChannelUID channelUID, final Command command) {
124 Coordinator localCoordinator = coordinator;
125 if (localCoordinator != null) {
126 localCoordinator.commander.handleCommand(channelUID, command);
128 logger.debug("Cannot handle command. No coordinator has been initialized");
132 @Override // Just exposing this for ChromecastStatusUpdater.
133 public void updateState(String channelId, State state) {
134 super.updateState(channelId, state);
137 @Override // Just exposing this for ChromecastStatusUpdater.
138 public void updateState(ChannelUID channelUID, State state) {
139 super.updateState(channelUID, state);
142 @Override // Just exposing this for ChromecastStatusUpdater.
143 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
144 super.updateStatus(status, statusDetail, description);
147 @Override // Just exposing this for ChromecastStatusUpdater.
148 public boolean isLinked(String channelId) {
149 return super.isLinked(channelId);
152 @Override // Just exposing this for ChromecastStatusUpdater.
153 public boolean isLinked(ChannelUID channelUID) {
154 return super.isLinked(channelUID);
158 public String getId() {
159 return thing.getUID().toString();
163 public @Nullable String getLabel(@Nullable Locale locale) {
164 return thing.getLabel();
168 public Set<AudioFormat> getSupportedFormats() {
169 return SUPPORTED_FORMATS;
173 public Set<Class<? extends AudioStream>> getSupportedStreams() {
174 return SUPPORTED_STREAMS;
178 public void process(@Nullable AudioStream audioStream)
179 throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
180 Coordinator localCoordinator = coordinator;
181 if (localCoordinator != null) {
182 localCoordinator.audioSink.process(audioStream);
184 logger.debug("Cannot process audioStream. No coordinator has been initialized.");
189 public PercentType getVolume() throws IOException {
190 Coordinator localCoordinator = coordinator;
191 if (localCoordinator != null) {
192 return localCoordinator.statusUpdater.getVolume();
194 throw new IOException("Cannot get volume. No coordinator has been initialized.");
199 public void setVolume(PercentType percentType) throws IOException {
200 Coordinator localCoordinator = coordinator;
201 if (localCoordinator != null) {
202 localCoordinator.commander.handleVolume(percentType);
204 throw new IOException("Cannot set volume. No coordinator has been initialized.");
208 private static class Coordinator {
209 private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
211 private static final long CONNECT_DELAY = 10;
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;
220 private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate,
221 AudioHTTPServer audioHttpServer, @Nullable String callbackURL) {
222 this.chromeCast = chromeCast;
224 this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
226 this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
228 this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
229 this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
230 this.audioSink = new ChromecastAudioSink(commander, audioHttpServer, callbackURL);
234 chromeCast.registerListener(eventReceiver);
235 chromeCast.registerConnectionListener(eventReceiver);
241 chromeCast.unregisterConnectionListener(eventReceiver);
242 chromeCast.unregisterListener(eventReceiver);
246 chromeCast.disconnect();
247 } catch (final IOException ex) {
248 logger.debug("Disconnect failed: {}", ex.getMessage());
252 private void connect() {
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,
260 scheduler.scheduleConnect();
264 private void refresh() {
265 commander.handleRefresh();