]> git.basschouten.com Git - openhab-addons.git/blob
71fdf633c62a6b0902a23fc0c5f8463da66e8959
[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         websocketID = websocketID.length() < 3 ? websocketID : websocketID.substring(websocketID.length() - 20);
87         this.websocket = new WebSocketConnection(this, webSocketFactory.createWebSocketClient(websocketID), gson);
88     }
89
90     @Override
91     public Collection<Class<? extends ThingHandlerService>> getServices() {
92         return Collections.singleton(ThingDiscoveryService.class);
93     }
94
95     @Override
96     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
97         if (!ignoreConfigurationUpdate) {
98             super.handleConfigurationUpdate(configurationParameters);
99         }
100     }
101
102     @Override
103     public void handleCommand(ChannelUID channelUID, Command command) {
104     }
105
106     /**
107      * Stops the API request or websocket reconnect timer
108      */
109     private void stopTimer() {
110         ScheduledFuture<?> future = scheduledFuture;
111         if (future != null) {
112             future.cancel(true);
113             scheduledFuture = null;
114         }
115     }
116
117     /**
118      * Parses the response message to the API key generation REST API.
119      *
120      * @param r The response
121      */
122     private void parseAPIKeyResponse(AsyncHttpClient.Result r) {
123         if (r.getResponseCode() == 403) {
124             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
125                     "Allow authentification for 3rd party apps. Trying again in " + POLL_FREQUENCY_SEC + " seconds");
126             stopTimer();
127             scheduledFuture = scheduler.schedule(() -> requestApiKey(), POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
128         } else if (r.getResponseCode() == 200) {
129             ApiKeyMessage[] response = gson.fromJson(r.getBody(), ApiKeyMessage[].class);
130             if (response.length == 0) {
131                 throw new IllegalStateException("Authorisation request response is empty");
132             }
133             config.apikey = response[0].success.username;
134             Configuration configuration = editConfiguration();
135             configuration.put(CONFIG_APIKEY, config.apikey);
136             updateConfiguration(configuration);
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Waiting for configuration");
138             requestFullState();
139         } else {
140             throw new IllegalStateException("Unknown status code for authorisation request");
141         }
142     }
143
144     /**
145      * Parses the response message to the REST API for retrieving the full bridge state with all sensors and switches
146      * and configuration.
147      *
148      * @param r The response
149      */
150     private @Nullable BridgeFullState parseBridgeFullStateResponse(AsyncHttpClient.Result r) {
151         if (r.getResponseCode() == 403) {
152             return null;
153         } else if (r.getResponseCode() == 200) {
154             return gson.fromJson(r.getBody(), BridgeFullState.class);
155         } else {
156             throw new IllegalStateException("Unknown status code for full state request");
157         }
158     }
159
160     /**
161      * Perform a request to the REST API for retrieving the full bridge state with all sensors and switches
162      * and configuration.
163      */
164     public void requestFullState() {
165         if (config.apikey == null) {
166             return;
167         }
168         String url = buildUrl(config.getHostWithoutPort(), config.httpPort, config.apikey);
169         http.get(url, config.timeout).thenApply(this::parseBridgeFullStateResponse).exceptionally(e -> {
170             if (e instanceof SocketTimeoutException || e instanceof TimeoutException
171                     || e instanceof CompletionException) {
172                 logger.debug("Get full state failed", e);
173             } else {
174                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
175             }
176             return null;
177         }).whenComplete((value, error) -> {
178             final ThingDiscoveryService thingDiscoveryService = this.thingDiscoveryService;
179             if (thingDiscoveryService != null) {
180                 // Hand over sensors to discovery service
181                 thingDiscoveryService.stateRequestFinished(value);
182             }
183         }).thenAccept(fullState -> {
184             if (fullState == null) {
185                 return;
186             }
187             if (fullState.config.name.isEmpty()) {
188                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
189                         "You are connected to a HUE bridge, not a deCONZ software!");
190                 return;
191             }
192             if (fullState.config.websocketport == 0) {
193                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
194                         "deCONZ software too old. No websocket support!");
195                 return;
196             }
197
198             // Add some information about the bridge
199             Map<String, String> editProperties = editProperties();
200             editProperties.put("apiversion", fullState.config.apiversion);
201             editProperties.put("swversion", fullState.config.swversion);
202             editProperties.put("fwversion", fullState.config.fwversion);
203             editProperties.put("uuid", fullState.config.uuid);
204             editProperties.put("zigbeechannel", String.valueOf(fullState.config.zigbeechannel));
205             editProperties.put("ipaddress", fullState.config.ipaddress);
206             ignoreConfigurationUpdate = true;
207             updateProperties(editProperties);
208             ignoreConfigurationUpdate = false;
209
210             // Use requested websocket port if no specific port is given
211             websocketPort = config.port == 0 ? fullState.config.websocketport : config.port;
212             websocketReconnect = true;
213             startWebsocket();
214         }).exceptionally(e -> {
215             if (e != null) {
216                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
217             } else {
218                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
219             }
220             logger.warn("Full state parsing failed", e);
221             return null;
222         });
223     }
224
225     /**
226      * Starts the websocket connection.
227      * {@link #requestFullState} need to be called first to obtain the websocket port.
228      */
229     private void startWebsocket() {
230         if (websocket.isConnected() || websocketPort == 0) {
231             return;
232         }
233
234         stopTimer();
235         scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
236
237         websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
238     }
239
240     /**
241      * Perform a request to the REST API for generating an API key.
242      *
243      */
244     private CompletableFuture<?> requestApiKey() {
245         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
246         stopTimer();
247         String url = buildUrl(config.getHostWithoutPort(), config.httpPort);
248         return http.post(url, "{\"devicetype\":\"openHAB\"}", config.timeout).thenAccept(this::parseAPIKeyResponse)
249                 .exceptionally(e -> {
250                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
251                     logger.warn("Authorisation failed", e);
252                     return null;
253                 });
254     }
255
256     @Override
257     public void initialize() {
258         logger.debug("Start initializing!");
259         config = getConfigAs(DeconzBridgeConfig.class);
260         if (config.apikey == null) {
261             requestApiKey();
262         } else {
263             requestFullState();
264         }
265     }
266
267     @Override
268     public void dispose() {
269         websocketReconnect = false;
270         stopTimer();
271         websocket.close();
272     }
273
274     @Override
275     public void connectionError(@Nullable Throwable e) {
276         if (e != null) {
277             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
278         } else {
279             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
280         }
281         stopTimer();
282         // Wait for POLL_FREQUENCY_SEC after a connection error before trying again
283         if (websocketReconnect) {
284             scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
285         }
286     }
287
288     @Override
289     public void connectionEstablished() {
290         stopTimer();
291         updateStatus(ThingStatus.ONLINE);
292     }
293
294     @Override
295     public void connectionLost(String reason) {
296         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
297         if (websocketReconnect) {
298             startWebsocket();
299         }
300     }
301
302     /**
303      * Return the websocket connection.
304      */
305     public WebSocketConnection getWebsocketConnection() {
306         return websocket;
307     }
308
309     /**
310      * Return the http connection.
311      */
312     public AsyncHttpClient getHttp() {
313         return http;
314     }
315
316     /**
317      * Return the bridge configuration.
318      */
319     public DeconzBridgeConfig getBridgeConfig() {
320         return config;
321     }
322
323     /**
324      * Called by the {@link ThingDiscoveryService}. Informs the bridge handler about the service.
325      *
326      * @param thingDiscoveryService The service
327      */
328     public void setDiscoveryService(ThingDiscoveryService thingDiscoveryService) {
329         this.thingDiscoveryService = thingDiscoveryService;
330     }
331 }