]> git.basschouten.com Git - openhab-addons.git/blob
b24be497ae1e441665b7938327599f20e6e084b0
[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(true);
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(boolean isInitialRequest) {
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                 if (isInitialRequest) {
186                     scheduledFuture = scheduler.schedule(() -> requestFullState(true), POLL_FREQUENCY_SEC,
187                             TimeUnit.SECONDS);
188                 }
189                 return;
190             }
191             if (fullState.config.name.isEmpty()) {
192                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
193                         "You are connected to a HUE bridge, not a deCONZ software!");
194                 return;
195             }
196             if (fullState.config.websocketport == 0) {
197                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
198                         "deCONZ software too old. No websocket support!");
199                 return;
200             }
201
202             // Add some information about the bridge
203             Map<String, String> editProperties = editProperties();
204             editProperties.put("apiversion", fullState.config.apiversion);
205             editProperties.put("swversion", fullState.config.swversion);
206             editProperties.put("fwversion", fullState.config.fwversion);
207             editProperties.put("uuid", fullState.config.uuid);
208             editProperties.put("zigbeechannel", String.valueOf(fullState.config.zigbeechannel));
209             editProperties.put("ipaddress", fullState.config.ipaddress);
210             ignoreConfigurationUpdate = true;
211             updateProperties(editProperties);
212             ignoreConfigurationUpdate = false;
213
214             // Use requested websocket port if no specific port is given
215             websocketPort = config.port == 0 ? fullState.config.websocketport : config.port;
216             websocketReconnect = true;
217             startWebsocket();
218         }).exceptionally(e -> {
219             if (e != null) {
220                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
221             } else {
222                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
223             }
224             logger.warn("Full state parsing failed", e);
225             return null;
226         });
227     }
228
229     /**
230      * Starts the websocket connection.
231      * {@link #requestFullState} need to be called first to obtain the websocket port.
232      */
233     private void startWebsocket() {
234         if (websocket.isConnected() || websocketPort == 0) {
235             return;
236         }
237
238         stopTimer();
239         scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
240
241         websocket.start(config.getHostWithoutPort() + ":" + websocketPort);
242     }
243
244     /**
245      * Perform a request to the REST API for generating an API key.
246      *
247      */
248     private CompletableFuture<?> requestApiKey() {
249         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Requesting API Key");
250         stopTimer();
251         String url = buildUrl(config.getHostWithoutPort(), config.httpPort);
252         return http.post(url, "{\"devicetype\":\"openHAB\"}", config.timeout).thenAccept(this::parseAPIKeyResponse)
253                 .exceptionally(e -> {
254                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
255                     logger.warn("Authorisation failed", e);
256                     return null;
257                 });
258     }
259
260     @Override
261     public void initialize() {
262         logger.debug("Start initializing!");
263         config = getConfigAs(DeconzBridgeConfig.class);
264         if (config.apikey == null) {
265             requestApiKey();
266         } else {
267             requestFullState(true);
268         }
269     }
270
271     @Override
272     public void dispose() {
273         websocketReconnect = false;
274         stopTimer();
275         websocket.close();
276     }
277
278     @Override
279     public void connectionError(@Nullable Throwable e) {
280         if (e != null) {
281             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
282         } else {
283             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unknown reason");
284         }
285         stopTimer();
286         // Wait for POLL_FREQUENCY_SEC after a connection error before trying again
287         if (websocketReconnect) {
288             scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS);
289         }
290     }
291
292     @Override
293     public void connectionEstablished() {
294         stopTimer();
295         updateStatus(ThingStatus.ONLINE);
296     }
297
298     @Override
299     public void connectionLost(String reason) {
300         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
301         if (websocketReconnect) {
302             startWebsocket();
303         }
304     }
305
306     /**
307      * Return the websocket connection.
308      */
309     public WebSocketConnection getWebsocketConnection() {
310         return websocket;
311     }
312
313     /**
314      * Return the http connection.
315      */
316     public AsyncHttpClient getHttp() {
317         return http;
318     }
319
320     /**
321      * Return the bridge configuration.
322      */
323     public DeconzBridgeConfig getBridgeConfig() {
324         return config;
325     }
326
327     /**
328      * Called by the {@link ThingDiscoveryService}. Informs the bridge handler about the service.
329      *
330      * @param thingDiscoveryService The service
331      */
332     public void setDiscoveryService(ThingDiscoveryService thingDiscoveryService) {
333         this.thingDiscoveryService = thingDiscoveryService;
334     }
335 }