2 * Copyright (c) 2010-2020 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.deconz.internal.handler;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.buildUrl;
18 import java.net.SocketTimeoutException;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.CompletionException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
29 import org.openhab.binding.deconz.internal.dto.ApiKeyMessage;
30 import org.openhab.binding.deconz.internal.dto.BridgeFullState;
31 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
32 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
33 import org.openhab.binding.deconz.internal.netutils.WebSocketConnectionListener;
34 import org.openhab.core.cache.ExpiringCacheAsync;
35 import org.openhab.core.config.core.Configuration;
36 import org.openhab.core.io.net.http.WebSocketFactory;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.ThingTypeUID;
42 import org.openhab.core.thing.binding.BaseBridgeHandler;
43 import org.openhab.core.thing.binding.ThingHandlerService;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.Gson;
51 * The bridge Thing is responsible for requesting all available sensors and switches and propagate
52 * them to the discovery service.
54 * It performs the authorization process if necessary.
56 * A websocket connection is established to the deCONZ software and kept alive.
58 * @author David Graeff - Initial contribution
61 public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener {
62 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
64 private final Logger logger = LoggerFactory.getLogger(DeconzBridgeHandler.class);
65 private final WebSocketConnection websocket;
66 private final AsyncHttpClient http;
67 private DeconzBridgeConfig config = new DeconzBridgeConfig();
68 private final Gson gson;
69 private @Nullable ScheduledFuture<?> scheduledFuture;
70 private int websocketPort = 0;
71 /** Prevent a dispose/init cycle while this flag is set. Use for property updates */
72 private boolean ignoreConfigurationUpdate;
73 private boolean websocketReconnect = false;
75 private final ExpiringCacheAsync<Optional<BridgeFullState>> fullStateCache = new ExpiringCacheAsync<>(1000);
77 /** The poll frequency for the API Key verification */
78 private static final int POLL_FREQUENCY_SEC = 10;
80 public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) {
84 String websocketID = thing.getUID().getAsString().replace(':', '-');
85 if (websocketID.length() < 4) {
86 websocketID = "openHAB-deconz-" + websocketID;
87 } else if (websocketID.length() > 20) {
88 websocketID = websocketID.substring(websocketID.length() - 20);
90 this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson);
94 public Collection<Class<? extends ThingHandlerService>> getServices() {
95 return Collections.singleton(ThingDiscoveryService.class);
99 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
100 if (!ignoreConfigurationUpdate) {
101 super.handleConfigurationUpdate(configurationParameters);
106 public void handleCommand(ChannelUID channelUID, Command command) {
110 * Stops the API request or websocket reconnect timer
112 private void stopTimer() {
113 ScheduledFuture<?> future = scheduledFuture;
114 if (future != null) {
116 scheduledFuture = null;
121 * Parses the response message to the API key generation REST API.
123 * @param r The response
125 private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
126 if (r.getResponseCode() == 403) {
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
128 "Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
130 scheduledFuture = scheduler.schedule(() -> requestApiKey(), POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
131 } else if (r.getResponseCode() == 200) {
132 ApiKeyMessage[] response = Objects.requireNonNull(gson.fromJson(r.getBody(), ApiKeyMessage[].class));
133 if (response.length == 0) {
134 throw new IllegalStateException("Authorisation request response is empty");
136 config.apikey = response[0].success.username;
137 Configuration configuration = editConfiguration();
138 configuration.put(CONFIG_APIKEY, config.apikey);
139 updateConfiguration(configuration);
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for configuration");
141 initializeBridgeState();
143 throw new IllegalStateException("Unknown status code for authorisation request");
148 * get the full state of the bridge from the cache
150 * @return a CompletableFuture that returns an Optional of the bridge full state
152 public CompletableFuture<Optional<BridgeFullState>> getBridgeFullState() {
153 return fullStateCache.getValue(this::refreshFullStateCache);
157 * refresh the full bridge state (used for initial processing and state-lookup)
159 * @return Completable future with an Optional of the BridgeFullState
161 private CompletableFuture<Optional<BridgeFullState>> refreshFullStateCache() {
162 logger.trace("{} starts refreshing the fullStateCache", thing.getUID());
163 if (config.apikey == null) {
164 return CompletableFuture.completedFuture(Optional.empty());
166 String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey);
167 return http.get(url, config.timeout).thenApply(r -> {
168 if (r.getResponseCode() == 403) {
169 return Optional.ofNullable((BridgeFullState) null);
170 } else if (r.getResponseCode() == 200) {
171 return Optional.ofNullable(gson.fromJson(r.getBody(), BridgeFullState.class));
173 throw new IllegalStateException("Unknown status code for full state request");
175 }).handle((v, t) -> {
178 } else if (t instanceof SocketTimeoutException || t instanceof TimeoutException
179 || t instanceof CompletionException) {
180 logger.debug("Get full state failed", t);
181 } else if (t != null) {
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, t.getMessage());
184 return Optional.empty();
189 * Perform a request to the REST API for retrieving the full bridge state with all sensors and switches
192 public void initializeBridgeState() {
193 getBridgeFullState().thenAccept(fullState -> fullState.ifPresentOrElse(state -> {
194 if (state.config.name.isEmpty()) {
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
196 "You are connected to a HUE bridge, not a deCONZ software!");
199 if (state.config.websocketport == 0) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
201 "deCONZ software too old. No websocket support!");
205 // Add some information about the bridge
206 Map<String, String> editProperties = editProperties();
207 editProperties.put("apiversion", state.config.apiversion);
208 editProperties.put("swversion", state.config.swversion);
209 editProperties.put("fwversion", state.config.fwversion);
210 editProperties.put("uuid", state.config.uuid);
211 editProperties.put("zigbeechannel", String.valueOf(state.config.zigbeechannel));
212 editProperties.put("ipaddress", state.config.ipaddress);
213 ignoreConfigurationUpdate = true;
214 updateProperties(editProperties);
215 ignoreConfigurationUpdate = false;
217 // Use requested websocket port if no specific port is given
218 websocketPort = config.port == 0 ? state.config.websocketport : config.port;
219 websocketReconnect = true;
222 // initial response was empty, re-trying in POLL_FREQUENCY_SEC seconds
223 scheduledFuture = scheduler.schedule(this::initializeBridgeState, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
224 })).exceptionally(e -> {
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
228 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
230 logger.warn("Initial full state parsing failed", e);
236 * Starts the websocket connection.
237 * {@link #initializeBridgeState} need to be called first to obtain the websocket port.
239 private void startWebsocket() {
240 if (websocket.isConnected() || websocketPort == 0 || websocketReconnect == false) {
245 scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
247 websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
251 * Perform a request to the REST API for generating an API key.
254 private CompletableFuture<?> requestApiKey() {
255 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
257 String url = buildUrl(config.getHostWithoutPort(), config.httpPort);
258 return http.post(url, "{\"devicetype\":\"openHAB\"}", config.timeout).thenAccept(this::parseAPIKeyResponse)
259 .exceptionally(e -> {
260 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
261 logger.warn("Authorisation failed", e);
267 public void initialize() {
268 logger.debug("Start initializing!");
269 config = getConfigAs(DeconzBridgeConfig.class);
270 if (config.apikey == null) {
273 initializeBridgeState();
278 public void dispose() {
279 websocketReconnect = false;
285 public void connectionError(@Nullable Throwable e) {
287 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
289 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
292 // Wait for POLL_FREQUENCY_SEC after a connection error before trying again
293 scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
297 public void connectionEstablished() {
299 updateStatus(ThingStatus.ONLINE);
303 public void connectionLost(String reason) {
304 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
309 * Return the websocket connection.
311 public WebSocketConnection getWebsocketConnection() {
316 * Return the http connection.
318 public AsyncHttpClient getHttp() {
323 * Return the bridge configuration.
325 public DeconzBridgeConfig getBridgeConfig() {