2 * Copyright (c) 2010-2023 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.security.GeneralSecurityException;
17 import java.util.Collection;
18 import java.util.List;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.chromecast.internal.ChromecastCommander;
23 import org.openhab.binding.chromecast.internal.ChromecastEventReceiver;
24 import org.openhab.binding.chromecast.internal.ChromecastScheduler;
25 import org.openhab.binding.chromecast.internal.ChromecastStatusUpdater;
26 import org.openhab.binding.chromecast.internal.action.ChromecastActions;
27 import org.openhab.binding.chromecast.internal.config.ChromecastConfig;
28 import org.openhab.core.audio.AudioSink;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.PercentType;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseThingHandler;
36 import org.openhab.core.thing.binding.ThingHandlerService;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.types.State;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import su.litvak.chromecast.api.v2.ChromeCast;
45 * The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
46 * furthermore implements {@link AudioSink} support.
48 * @author Markus Rathgeb, Kai Kreuzer - Initial contribution
49 * @author Daniel Walters - Online status fix, handle playuri channel and refactor play media code
50 * @author Jason Holmes - Media Status. Refactor the monolith into separate classes.
51 * @author Scott Hanson - Added Actions.
54 public class ChromecastHandler extends BaseThingHandler {
55 private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
58 * The actual implementation. A new one is created each time #initialize is called.
60 private @Nullable Coordinator coordinator;
65 * @param thing the thing the coordinator should be created for
67 public ChromecastHandler(final Thing thing) {
72 public void initialize() {
73 ChromecastConfig config = getConfigAs(ChromecastConfig.class);
75 final String ipAddress = config.ipAddress;
76 if (ipAddress == null || ipAddress.isBlank()) {
77 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
78 "Cannot connect to Chromecast. IP address is not valid or missing.");
82 updateStatus(ThingStatus.UNKNOWN);
84 Coordinator localCoordinator = coordinator;
85 if (localCoordinator != null && (!localCoordinator.chromeCast.getAddress().equals(ipAddress)
86 || (localCoordinator.chromeCast.getPort() != config.port))) {
87 localCoordinator.destroy();
88 localCoordinator = coordinator = null;
91 if (localCoordinator == null) {
92 ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
93 localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate);
94 coordinator = localCoordinator;
96 scheduler.submit(() -> {
97 Coordinator c = coordinator;
106 public void dispose() {
107 Coordinator localCoordinator = coordinator;
108 if (localCoordinator != null) {
109 localCoordinator.destroy();
115 public void handleCommand(final ChannelUID channelUID, final Command command) {
116 Coordinator localCoordinator = coordinator;
117 if (localCoordinator != null) {
118 localCoordinator.commander.handleCommand(channelUID, command);
120 logger.debug("Cannot handle command. No coordinator has been initialized");
124 @Override // Just exposing this for ChromecastStatusUpdater.
125 public void updateState(String channelId, State state) {
126 super.updateState(channelId, state);
129 @Override // Just exposing this for ChromecastStatusUpdater.
130 public void updateState(ChannelUID channelUID, State state) {
131 super.updateState(channelUID, state);
134 @Override // Just exposing this for ChromecastStatusUpdater.
135 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
136 super.updateStatus(status, statusDetail, description);
139 @Override // Just exposing this for ChromecastStatusUpdater.
140 public boolean isLinked(String channelId) {
141 return super.isLinked(channelId);
144 @Override // Just exposing this for ChromecastStatusUpdater.
145 public boolean isLinked(ChannelUID channelUID) {
146 return super.isLinked(channelUID);
149 public PercentType getVolume() throws IOException {
150 Coordinator localCoordinator = coordinator;
151 if (localCoordinator != null) {
152 return localCoordinator.statusUpdater.getVolume();
154 throw new IOException("Cannot get volume. No coordinator has been initialized.");
158 public void setVolume(PercentType percentType) throws IOException {
159 Coordinator localCoordinator = coordinator;
160 if (localCoordinator != null) {
161 localCoordinator.commander.handleVolume(percentType);
163 throw new IOException("Cannot set volume. No coordinator has been initialized.");
168 Coordinator localCoordinator = coordinator;
169 if (localCoordinator != null) {
170 localCoordinator.commander.handleCloseApp(OnOffType.ON);
172 logger.debug("Cannot stop. No coordinator has been initialized.");
177 public Collection<Class<? extends ThingHandlerService>> getServices() {
178 return List.of(ChromecastActions.class);
181 public boolean playURL(@Nullable String title, String url, @Nullable String mediaType) {
182 Coordinator localCoordinator = coordinator;
183 if (localCoordinator != null) {
184 localCoordinator.commander.playMedia(title, url, mediaType);
190 private static class Coordinator {
191 private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
193 private static final long CONNECT_DELAY = 10;
195 private final ChromeCast chromeCast;
196 private final ChromecastCommander commander;
197 private final ChromecastEventReceiver eventReceiver;
198 private final ChromecastStatusUpdater statusUpdater;
199 private final ChromecastScheduler scheduler;
202 * used internally to represent the connection state
204 private enum ConnectionState {
212 private ConnectionState connectionState = ConnectionState.UNKNOWN;
214 private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate) {
215 this.chromeCast = chromeCast;
217 this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
219 this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
221 this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
222 this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
226 if (connectionState == ConnectionState.CONNECTED) {
227 logger.debug("Already connected");
229 } else if (connectionState == ConnectionState.CONNECTING) {
230 logger.debug("Already connecting");
232 } else if (connectionState == ConnectionState.DISCONNECTING) {
233 logger.warn("Trying to re-connect while still disconnecting");
236 connectionState = ConnectionState.CONNECTING;
238 chromeCast.registerListener(eventReceiver);
239 chromeCast.registerConnectionListener(eventReceiver);
245 connectionState = ConnectionState.DISCONNECTING;
247 chromeCast.unregisterConnectionListener(eventReceiver);
248 chromeCast.unregisterListener(eventReceiver);
253 chromeCast.disconnect();
255 connectionState = ConnectionState.DISCONNECTED;
256 } catch (final IOException e) {
257 logger.debug("Disconnect failed: {}", e.getMessage());
258 connectionState = ConnectionState.UNKNOWN;
262 private void connect() {
264 chromeCast.connect();
266 statusUpdater.updateMediaStatus(null);
267 statusUpdater.updateStatus(ThingStatus.ONLINE);
269 connectionState = ConnectionState.CONNECTED;
270 } catch (final IOException | GeneralSecurityException e) {
271 logger.debug("Connect failed, trying to reconnect: {}", e.getMessage());
272 statusUpdater.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
274 scheduler.scheduleConnect();
278 private void refresh() {
279 commander.handleRefresh();