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;
19 import java.util.Collection;
20 import java.util.Collections;
23 import java.util.concurrent.CompletableFuture;
24 import java.util.concurrent.CompletionException;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.deconz.internal.discovery.ThingDiscoveryService;
32 import org.openhab.binding.deconz.internal.dto.ApiKeyMessage;
33 import org.openhab.binding.deconz.internal.dto.BridgeFullState;
34 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
35 import org.openhab.binding.deconz.internal.netutils.WebSocketConnection;
36 import org.openhab.binding.deconz.internal.netutils.WebSocketConnectionListener;
37 import org.openhab.core.config.core.Configuration;
38 import org.openhab.core.io.net.http.WebSocketFactory;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.openhab.core.types.Command;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.Gson;
53 * The bridge Thing is responsible for requesting all available sensors and switches and propagate
54 * them to the discovery service.
56 * It performs the authorization process if necessary.
58 * A websocket connection is established to the deCONZ software and kept alive.
60 * @author David Graeff - Initial contribution
63 public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener {
64 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
66 private final Logger logger = LoggerFactory.getLogger(DeconzBridgeHandler.class);
67 private @Nullable ThingDiscoveryService thingDiscoveryService;
68 private final WebSocketConnection websocket;
69 private final AsyncHttpClient http;
70 private DeconzBridgeConfig config = new DeconzBridgeConfig();
71 private final Gson gson;
72 private @Nullable ScheduledFuture<?> scheduledFuture;
73 private int websocketPort = 0;
74 /** Prevent a dispose/init cycle while this flag is set. Use for property updates */
75 private boolean ignoreConfigurationUpdate;
76 private boolean websocketReconnect = false;
78 /** The poll frequency for the API Key verification */
79 private static final int POLL_FREQUENCY_SEC = 10;
81 public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) {
85 String websocketID = thing.getUID().getAsString().replace(':', '-');
86 if (websocketID.length() < 4) {
87 websocketID = "openHAB-deconz-" + websocketID;
88 } else if (websocketID.length() > 20) {
89 websocketID = websocketID.substring(websocketID.length() - 20);
91 this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson);
95 public Collection<Class<? extends ThingHandlerService>> getServices() {
96 return Collections.singleton(ThingDiscoveryService.class);
100 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
101 if (!ignoreConfigurationUpdate) {
102 super.handleConfigurationUpdate(configurationParameters);
107 public void handleCommand(ChannelUID channelUID, Command command) {
111 * Stops the API request or websocket reconnect timer
113 private void stopTimer() {
114 ScheduledFuture<?> future = scheduledFuture;
115 if (future != null) {
117 scheduledFuture = null;
122 * Parses the response message to the API key generation REST API.
124 * @param r The response
126 private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
127 if (r.getResponseCode() == 403) {
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
129 "Allow authentication for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
131 scheduledFuture = scheduler.schedule(() -> requestApiKey(), POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
132 } else if (r.getResponseCode() == 200) {
133 ApiKeyMessage[] response = gson.fromJson(r.getBody(), ApiKeyMessage[].class);
134 if (response.length == 0) {
135 throw new IllegalStateException("Authorisation request response is empty");
137 config.apikey = response[0].success.username;
138 Configuration configuration = editConfiguration();
139 configuration.put(CONFIG_APIKEY, config.apikey);
140 updateConfiguration(configuration);
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for configuration");
142 requestFullState(true);
144 throw new IllegalStateException("Unknown status code for authorisation request");
149 * Parses the response message to the REST API for retrieving the full bridge state with all sensors and switches
152 * @param r The response
154 private @Nullable BridgeFullState parseBridgeFullStateResponse(AsyncHttpClient.Result r) {
155 if (r.getResponseCode() == 403) {
157 } else if (r.getResponseCode() == 200) {
158 return gson.fromJson(r.getBody(), BridgeFullState.class);
160 throw new IllegalStateException("Unknown status code for full state request");
165 * Perform a request to the REST API for retrieving the full bridge state with all sensors and switches
168 public void requestFullState(boolean isInitialRequest) {
169 if (config.apikey == null) {
172 String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey);
173 http.get(url, config.timeout).thenApply(this::parseBridgeFullStateResponse).exceptionally(e -> {
174 if (e instanceof SocketTimeoutException || e instanceof TimeoutException
175 || e instanceof CompletionException) {
176 logger.debug("Get full state failed", e);
178 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
181 }).whenComplete((value, error) -> {
182 final ThingDiscoveryService thingDiscoveryService = this.thingDiscoveryService;
183 if (thingDiscoveryService != null) {
184 // Hand over sensors to discovery service
185 thingDiscoveryService.stateRequestFinished(value);
187 }).thenAccept(fullState -> {
188 if (fullState == null) {
189 if (isInitialRequest) {
190 scheduledFuture = scheduler.schedule(() -> requestFullState(true), POLL_FREQUENCY_SEC,
195 if (fullState.config.name.isEmpty()) {
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
197 "You are connected to a HUE bridge, not a deCONZ software!");
200 if (fullState.config.websocketport == 0) {
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
202 "deCONZ software too old. No websocket support!");
206 // Add some information about the bridge
207 Map<String, String> editProperties = editProperties();
208 editProperties.put("apiversion", fullState.config.apiversion);
209 editProperties.put("swversion", fullState.config.swversion);
210 editProperties.put("fwversion", fullState.config.fwversion);
211 editProperties.put("uuid", fullState.config.uuid);
212 editProperties.put("zigbeechannel", String.valueOf(fullState.config.zigbeechannel));
213 editProperties.put("ipaddress", fullState.config.ipaddress);
214 ignoreConfigurationUpdate = true;
215 updateProperties(editProperties);
216 ignoreConfigurationUpdate = false;
218 // Use requested websocket port if no specific port is given
219 websocketPort = config.port == 0 ? fullState.config.websocketport : config.port;
220 websocketReconnect = true;
222 }).exceptionally(e -> {
224 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
228 logger.warn("Full state parsing failed", e);
234 * Starts the websocket connection.
236 * {@link #requestFullState} need to be called first to obtain the websocket port.
238 private void startWebsocket() {
239 if (websocket.isConnected() || websocketPort == 0 || websocketReconnect == false) {
244 scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
246 websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
250 * Perform a request to the REST API for generating an API key.
253 private CompletableFuture<?> requestApiKey() {
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
256 String url = buildUrl(config.getHostWithoutPort(), config.httpPort);
257 return http.post(url, "{\"devicetype\":\"openHAB\"}", config.timeout).thenAccept(this::parseAPIKeyResponse)
258 .exceptionally(e -> {
259 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
260 logger.warn("Authorisation failed", e);
266 public void initialize() {
267 logger.debug("Start initializing!");
268 config = getConfigAs(DeconzBridgeConfig.class);
269 if (config.apikey == null) {
272 requestFullState(true);
277 public void dispose() {
278 websocketReconnect = false;
284 public void connectionError(@Nullable Throwable e) {
286 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
288 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
291 // Wait for POLL_FREQUENCY_SEC after a connection error before trying again
292 scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
296 public void connectionEstablished() {
298 updateStatus(ThingStatus.ONLINE);
302 public void connectionLost(String reason) {
303 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
308 * Return the websocket connection.
310 public WebSocketConnection getWebsocketConnection() {
315 * Return the http connection.
317 public AsyncHttpClient getHttp() {
322 * Return the bridge configuration.
324 public DeconzBridgeConfig getBridgeConfig() {
329 * Called by the {@link ThingDiscoveryService}. Informs the bridge handler about the service.
331 * @param thingDiscoveryService The service
333 public void setDiscoveryService(ThingDiscoveryService thingDiscoveryService) {
334 this.thingDiscoveryService = thingDiscoveryService;