]> git.basschouten.com Git - openhab-addons.git/blob
fcedf3dd48824cfc79a8bc50c70ada0046287e84
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.security.GeneralSecurityException;
17 import java.util.Collection;
18 import java.util.List;
19
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;
41
42 import su.litvak.chromecast.api.v2.ChromeCast;
43
44 /**
45  * The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
46  * furthermore implements {@link AudioSink} support.
47  *
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.
52  */
53 @NonNullByDefault
54 public class ChromecastHandler extends BaseThingHandler {
55     private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
56
57     /**
58      * The actual implementation. A new one is created each time #initialize is called.
59      */
60     private @Nullable Coordinator coordinator;
61
62     /**
63      * Constructor.
64      *
65      * @param thing the thing the coordinator should be created for
66      */
67     public ChromecastHandler(final Thing thing) {
68         super(thing);
69     }
70
71     @Override
72     public void initialize() {
73         ChromecastConfig config = getConfigAs(ChromecastConfig.class);
74
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.");
79             return;
80         }
81
82         updateStatus(ThingStatus.UNKNOWN);
83
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;
89         }
90
91         if (localCoordinator == null) {
92             ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
93             localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate);
94             coordinator = localCoordinator;
95
96             scheduler.submit(() -> {
97                 Coordinator c = coordinator;
98                 if (c != null) {
99                     c.initialize();
100                 }
101             });
102         }
103     }
104
105     @Override
106     public void dispose() {
107         Coordinator localCoordinator = coordinator;
108         if (localCoordinator != null) {
109             localCoordinator.destroy();
110             coordinator = null;
111         }
112     }
113
114     @Override
115     public void handleCommand(final ChannelUID channelUID, final Command command) {
116         Coordinator localCoordinator = coordinator;
117         if (localCoordinator != null) {
118             localCoordinator.commander.handleCommand(channelUID, command);
119         } else {
120             logger.debug("Cannot handle command. No coordinator has been initialized");
121         }
122     }
123
124     @Override // Just exposing this for ChromecastStatusUpdater.
125     public void updateState(String channelId, State state) {
126         super.updateState(channelId, state);
127     }
128
129     @Override // Just exposing this for ChromecastStatusUpdater.
130     public void updateState(ChannelUID channelUID, State state) {
131         super.updateState(channelUID, state);
132     }
133
134     @Override // Just exposing this for ChromecastStatusUpdater.
135     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
136         super.updateStatus(status, statusDetail, description);
137     }
138
139     @Override // Just exposing this for ChromecastStatusUpdater.
140     public boolean isLinked(String channelId) {
141         return super.isLinked(channelId);
142     }
143
144     @Override // Just exposing this for ChromecastStatusUpdater.
145     public boolean isLinked(ChannelUID channelUID) {
146         return super.isLinked(channelUID);
147     }
148
149     public PercentType getVolume() throws IOException {
150         Coordinator localCoordinator = coordinator;
151         if (localCoordinator != null) {
152             return localCoordinator.statusUpdater.getVolume();
153         } else {
154             throw new IOException("Cannot get volume. No coordinator has been initialized.");
155         }
156     }
157
158     public void setVolume(PercentType percentType) throws IOException {
159         Coordinator localCoordinator = coordinator;
160         if (localCoordinator != null) {
161             localCoordinator.commander.handleVolume(percentType);
162         } else {
163             throw new IOException("Cannot set volume. No coordinator has been initialized.");
164         }
165     }
166
167     public void stop() {
168         Coordinator localCoordinator = coordinator;
169         if (localCoordinator != null) {
170             localCoordinator.commander.handleCloseApp(OnOffType.ON);
171         } else {
172             logger.debug("Cannot stop. No coordinator has been initialized.");
173         }
174     }
175
176     @Override
177     public Collection<Class<? extends ThingHandlerService>> getServices() {
178         return List.of(ChromecastActions.class);
179     }
180
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);
185             return true;
186         }
187         return false;
188     }
189
190     private static class Coordinator {
191         private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
192
193         private static final long CONNECT_DELAY = 10;
194
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;
200
201         /**
202          * used internally to represent the connection state
203          */
204         private enum ConnectionState {
205             UNKNOWN,
206             CONNECTING,
207             CONNECTED,
208             DISCONNECTING,
209             DISCONNECTED
210         }
211
212         private ConnectionState connectionState = ConnectionState.UNKNOWN;
213
214         private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate) {
215             this.chromeCast = chromeCast;
216
217             this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
218                     this::refresh);
219             this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
220
221             this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
222             this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
223         }
224
225         void initialize() {
226             if (connectionState == ConnectionState.CONNECTED) {
227                 logger.debug("Already connected");
228                 return;
229             } else if (connectionState == ConnectionState.CONNECTING) {
230                 logger.debug("Already connecting");
231                 return;
232             } else if (connectionState == ConnectionState.DISCONNECTING) {
233                 logger.warn("Trying to re-connect while still disconnecting");
234                 return;
235             }
236             connectionState = ConnectionState.CONNECTING;
237
238             chromeCast.registerListener(eventReceiver);
239             chromeCast.registerConnectionListener(eventReceiver);
240
241             connect();
242         }
243
244         void destroy() {
245             connectionState = ConnectionState.DISCONNECTING;
246
247             chromeCast.unregisterConnectionListener(eventReceiver);
248             chromeCast.unregisterListener(eventReceiver);
249
250             scheduler.destroy();
251
252             try {
253                 chromeCast.disconnect();
254
255                 connectionState = ConnectionState.DISCONNECTED;
256             } catch (final IOException e) {
257                 logger.debug("Disconnect failed: {}", e.getMessage());
258                 connectionState = ConnectionState.UNKNOWN;
259             }
260         }
261
262         private void connect() {
263             try {
264                 chromeCast.connect();
265
266                 statusUpdater.updateMediaStatus(null);
267                 statusUpdater.updateStatus(ThingStatus.ONLINE);
268
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,
273                         e.getMessage());
274                 scheduler.scheduleConnect();
275             }
276         }
277
278         private void refresh() {
279             commander.handleRefresh();
280         }
281     }
282 }