]> git.basschouten.com Git - openhab-addons.git/blob
7955e11fed33fc72bd37084c400e897dd94933d2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.deconz.internal.handler;
14
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.buildUrl;
17
18 import java.net.SocketTimeoutException;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.Map;
22 import java.util.Set;
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;
28
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;
49
50 import com.google.gson.Gson;
51
52 /**
53  * The bridge Thing is responsible for requesting all available sensors and switches and propagate
54  * them to the discovery service.
55  *
56  * It performs the authorization process if necessary.
57  *
58  * A websocket connection is established to the deCONZ software and kept alive.
59  *
60  * @author David Graeff - Initial contribution
61  */
62 @NonNullByDefault
63 public class DeconzBridgeHandler extends BaseBridgeHandler implements WebSocketConnectionListener {
64     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
65
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;
77
78     /** The poll frequency for the API Key verification */
79     private static final int POLL_FREQUENCY_SEC = 10;
80
81     public DeconzBridgeHandler(Bridge thing, WebSocketFactory webSocketFactory, AsyncHttpClient http, Gson gson) {
82         super(thing);
83         this.http = http;
84         this.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);
90         }
91         this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson);
92     }
93
94     @Override
95     public Collection<Class<? extends ThingHandlerService>> getServices() {
96         return Collections.singleton(ThingDiscoveryService.class);
97     }
98
99     @Override
100     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
101         if (!ignoreConfigurationUpdate) {
102             super.handleConfigurationUpdate(configurationParameters);
103         }
104     }
105
106     @Override
107     public void handleCommand(ChannelUID channelUID, Command command) {
108     }
109
110     /**
111      * Stops the API request or websocket reconnect timer
112      */
113     private void stopTimer() {
114         ScheduledFuture<?> future = scheduledFuture;
115         if (future != null) {
116             future.cancel(true);
117             scheduledFuture = null;
118         }
119     }
120
121     /**
122      * Parses the response message to the API key generation REST API.
123      *
124      * @param r The response
125      */
126     private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
127         if (r.getResponseCode() == 403) {
128             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
129                     "Allow authentification for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
130             stopTimer();
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");
136             }
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);
143         } else {
144             throw new IllegalStateException("Unknown status code for authorisation request");
145         }
146     }
147
148     /**
149      * Parses the response message to the REST API for retrieving the full bridge state with all sensors and switches
150      * and configuration.
151      *
152      * @param r The response
153      */
154     private @Nullable BridgeFullState parseBridgeFullStateResponse(AsyncHttpClient.Result r) {
155         if (r.getResponseCode() == 403) {
156             return null;
157         } else if (r.getResponseCode() == 200) {
158             return gson.fromJson(r.getBody(), BridgeFullState.class);
159         } else {
160             throw new IllegalStateException("Unknown status code for full state request");
161         }
162     }
163
164     /**
165      * Perform a request to the REST API for retrieving the full bridge state with all sensors and switches
166      * and configuration.
167      */
168     public void requestFullState(boolean isInitialRequest) {
169         if (config.apikey == null) {
170             return;
171         }
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);
177             } else {
178                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
179             }
180             return null;
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);
186             }
187         }).thenAccept(fullState -> {
188             if (fullState == null) {
189                 if (isInitialRequest) {
190                     scheduledFuture = scheduler.schedule(() -> requestFullState(true), POLL_FREQUENCY_SEC,
191                             TimeUnit.SECONDS);
192                 }
193                 return;
194             }
195             if (fullState.config.name.isEmpty()) {
196                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
197                         "You are connected to a HUE bridge, not a deCONZ software!");
198                 return;
199             }
200             if (fullState.config.websocketport == 0) {
201                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
202                         "deCONZ software too old. No websocket support!");
203                 return;
204             }
205
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;
217
218             // Use requested websocket port if no specific port is given
219             websocketPort = config.port == 0 ? fullState.config.websocketport : config.port;
220             websocketReconnect = true;
221             startWebsocket();
222         }).exceptionally(e -> {
223             if (e != null) {
224                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
225             } else {
226                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
227             }
228             logger.warn("Full state parsing failed", e);
229             return null;
230         });
231     }
232
233     /**
234      * Starts the websocket connection.
235      *
236      * {@link #requestFullState} need to be called first to obtain the websocket port.
237      */
238     private void startWebsocket() {
239         if (websocket.isConnected() || websocketPort == 0 || websocketReconnect == false) {
240             return;
241         }
242
243         stopTimer();
244         scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
245
246         websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
247     }
248
249     /**
250      * Perform a request to the REST API for generating an API key.
251      *
252      */
253     private CompletableFuture<?> requestApiKey() {
254         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
255         stopTimer();
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);
261                     return null;
262                 });
263     }
264
265     @Override
266     public void initialize() {
267         logger.debug("Start initializing!");
268         config = getConfigAs(DeconzBridgeConfig.class);
269         if (config.apikey == null) {
270             requestApiKey();
271         } else {
272             requestFullState(true);
273         }
274     }
275
276     @Override
277     public void dispose() {
278         websocketReconnect = false;
279         stopTimer();
280         websocket.close();
281     }
282
283     @Override
284     public void connectionError(@Nullable Throwable e) {
285         if (e != null) {
286             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
287         } else {
288             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
289         }
290         stopTimer();
291         // Wait for POLL_FREQUENCY_SEC after a connection error before trying again
292         scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
293     }
294
295     @Override
296     public void connectionEstablished() {
297         stopTimer();
298         updateStatus(ThingStatus.ONLINE);
299     }
300
301     @Override
302     public void connectionLost(String reason) {
303         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
304         startWebsocket();
305     }
306
307     /**
308      * Return the websocket connection.
309      */
310     public WebSocketConnection getWebsocketConnection() {
311         return websocket;
312     }
313
314     /**
315      * Return the http connection.
316      */
317     public AsyncHttpClient getHttp() {
318         return http;
319     }
320
321     /**
322      * Return the bridge configuration.
323      */
324     public DeconzBridgeConfig getBridgeConfig() {
325         return config;
326     }
327
328     /**
329      * Called by the {@link ThingDiscoveryService}. Informs the bridge handler about the service.
330      *
331      * @param thingDiscoveryService The service
332      */
333     public void setDiscoveryService(ThingDiscoveryService thingDiscoveryService) {
334         this.thingDiscoveryService = thingDiscoveryService;
335     }
336 }