]> git.basschouten.com Git - openhab-addons.git/blob
6ea12fad69a906026b4b95140382dbd305a0c848
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.tradfri.internal.handler;
14
15 import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Objects;
23 import java.util.Set;
24 import java.util.UUID;
25 import java.util.concurrent.CopyOnWriteArraySet;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.californium.core.CoapClient;
30 import org.eclipse.californium.core.CoapResponse;
31 import org.eclipse.californium.core.network.CoapEndpoint;
32 import org.eclipse.californium.elements.exception.ConnectorException;
33 import org.eclipse.californium.scandium.DTLSConnector;
34 import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
35 import org.eclipse.californium.scandium.dtls.pskstore.AdvancedSinglePskStore;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.tradfri.internal.CoapCallback;
39 import org.openhab.binding.tradfri.internal.DeviceUpdateListener;
40 import org.openhab.binding.tradfri.internal.TradfriBindingConstants;
41 import org.openhab.binding.tradfri.internal.TradfriCoapClient;
42 import org.openhab.binding.tradfri.internal.TradfriCoapHandler;
43 import org.openhab.binding.tradfri.internal.config.TradfriGatewayConfig;
44 import org.openhab.binding.tradfri.internal.discovery.TradfriDiscoveryService;
45 import org.openhab.binding.tradfri.internal.model.TradfriVersion;
46 import org.openhab.core.config.core.Configuration;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.binding.BaseBridgeHandler;
53 import org.openhab.core.thing.binding.ThingHandler;
54 import org.openhab.core.thing.binding.ThingHandlerService;
55 import org.openhab.core.types.Command;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59 import com.google.gson.JsonArray;
60 import com.google.gson.JsonElement;
61 import com.google.gson.JsonObject;
62 import com.google.gson.JsonParseException;
63 import com.google.gson.JsonParser;
64 import com.google.gson.JsonSyntaxException;
65
66 /**
67  * The {@link TradfriGatewayHandler} is responsible for handling commands, which are
68  * sent to one of the channels.
69  *
70  * @author Kai Kreuzer - Initial contribution
71  */
72 @NonNullByDefault
73 public class TradfriGatewayHandler extends BaseBridgeHandler implements CoapCallback {
74
75     protected final Logger logger = LoggerFactory.getLogger(getClass());
76
77     private static final TradfriVersion MIN_SUPPORTED_VERSION = new TradfriVersion("1.2.42");
78
79     private @NonNullByDefault({}) TradfriCoapClient deviceClient;
80     private @NonNullByDefault({}) String gatewayURI;
81     private @NonNullByDefault({}) String gatewayInfoURI;
82     private @NonNullByDefault({}) DTLSConnector dtlsConnector;
83     private @Nullable CoapEndpoint endPoint;
84
85     private final Set<DeviceUpdateListener> deviceUpdateListeners = new CopyOnWriteArraySet<>();
86
87     private @Nullable ScheduledFuture<?> scanJob;
88
89     public TradfriGatewayHandler(Bridge bridge) {
90         super(bridge);
91     }
92
93     @Override
94     public void handleCommand(ChannelUID channelUID, Command command) {
95         // there are no channels on the gateway yet
96     }
97
98     @Override
99     public void initialize() {
100         TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
101
102         if (isNullOrEmpty(configuration.host)) {
103             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
104                     "Host must be specified in the configuration!");
105             return;
106         }
107
108         if (isNullOrEmpty(configuration.code)) {
109             if (isNullOrEmpty(configuration.identity) || isNullOrEmpty(configuration.preSharedKey)) {
110                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111                         "Either security code or identity and pre-shared key must be provided in the configuration!");
112                 return;
113             } else {
114                 establishConnection();
115             }
116         } else {
117             String currentFirmware = thing.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION);
118             if (!isNullOrEmpty(currentFirmware) && MIN_SUPPORTED_VERSION
119                     .compareTo(new TradfriVersion(Objects.requireNonNull(currentFirmware))) > 0) {
120                 // older firmware not supported
121                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
122                         String.format(
123                                 "Gateway firmware version '%s' is too old! Minimum supported firmware version is '%s'.",
124                                 currentFirmware, MIN_SUPPORTED_VERSION.toString()));
125                 return;
126             }
127
128             // Running async operation to retrieve new <'identity','key'> pair
129             scheduler.execute(() -> {
130                 boolean success = obtainIdentityAndPreSharedKey();
131                 if (success) {
132                     establishConnection();
133                 }
134             });
135         }
136     }
137
138     @Override
139     public Collection<Class<? extends ThingHandlerService>> getServices() {
140         return Collections.singleton(TradfriDiscoveryService.class);
141     }
142
143     private void establishConnection() {
144         TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
145
146         this.gatewayURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + DEVICES;
147         this.gatewayInfoURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + GATEWAY + "/"
148                 + GATEWAY_DETAILS;
149         try {
150             URI uri = new URI(gatewayURI);
151             deviceClient = new TradfriCoapClient(uri);
152         } catch (URISyntaxException e) {
153             logger.error("Illegal gateway URI '{}': {}", gatewayURI, e.getMessage());
154             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
155             return;
156         }
157
158         DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
159         builder.setAdvancedPskStore(
160                 new AdvancedSinglePskStore(configuration.identity, configuration.preSharedKey.getBytes()));
161         builder.setMaxConnections(100);
162         builder.setStaleConnectionThreshold(60);
163         dtlsConnector = new DTLSConnector(builder.build());
164         endPoint = new CoapEndpoint.Builder().setConnector(dtlsConnector).build();
165         deviceClient.setEndpoint(endPoint);
166         updateStatus(ThingStatus.UNKNOWN);
167
168         // schedule a new scan every minute
169         scanJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 1, TimeUnit.MINUTES);
170     }
171
172     /**
173      * Authenticates against the gateway with the security code in order to receive a pre-shared key for a newly
174      * generated identity.
175      * As this requires a remote request, this method might be long-running.
176      *
177      * @return true, if credentials were successfully obtained, false otherwise
178      */
179     protected boolean obtainIdentityAndPreSharedKey() {
180         TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
181
182         String identity = UUID.randomUUID().toString().replace("-", "");
183         String preSharedKey = null;
184
185         CoapResponse gatewayResponse;
186         String authUrl = null;
187         String responseText = null;
188         try {
189             DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder();
190             builder.setAdvancedPskStore(new AdvancedSinglePskStore("Client_identity", configuration.code.getBytes()));
191
192             DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
193             CoapEndpoint.Builder authEndpointBuilder = new CoapEndpoint.Builder();
194             authEndpointBuilder.setConnector(dtlsConnector);
195             CoapEndpoint authEndpoint = authEndpointBuilder.build();
196             authUrl = "coaps://" + configuration.host + ":" + configuration.port + "/15011/9063";
197
198             CoapClient deviceClient = new CoapClient(new URI(authUrl));
199             deviceClient.setTimeout(TimeUnit.SECONDS.toMillis(10));
200             deviceClient.setEndpoint(authEndpoint);
201
202             JsonObject json = new JsonObject();
203             json.addProperty(CLIENT_IDENTITY_PROPOSED, identity);
204
205             gatewayResponse = deviceClient.post(json.toString(), 0);
206
207             authEndpoint.destroy();
208             deviceClient.shutdown();
209
210             if (gatewayResponse == null) {
211                 // seems we ran in a timeout, which potentially also happens
212                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
213                         "No response from gateway. Might be due to an invalid security code.");
214                 return false;
215             }
216
217             if (gatewayResponse.isSuccess()) {
218                 responseText = gatewayResponse.getResponseText();
219                 json = JsonParser.parseString(responseText).getAsJsonObject();
220                 preSharedKey = json.get(NEW_PSK_BY_GW).getAsString();
221
222                 if (isNullOrEmpty(preSharedKey)) {
223                     logger.error("Received pre-shared key is empty for thing {} on gateway at {}", getThing().getUID(),
224                             configuration.host);
225                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
226                             "Pre-shared key was not obtain successfully");
227                     return false;
228                 } else {
229                     logger.debug("Received pre-shared key for gateway '{}'", configuration.host);
230                     logger.debug("Using identity '{}' with pre-shared key '{}'.", identity, preSharedKey);
231
232                     Configuration editedConfig = editConfiguration();
233                     editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_CODE, null);
234                     editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_IDENTITY, identity);
235                     editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_PRE_SHARED_KEY, preSharedKey);
236                     updateConfiguration(editedConfig);
237
238                     return true;
239                 }
240             } else {
241                 logger.warn(
242                         "Failed obtaining pre-shared key for identity '{}' (response code '{}', response text '{}')",
243                         identity, gatewayResponse.getCode(),
244                         isNullOrEmpty(gatewayResponse.getResponseText()) ? "<empty>"
245                                 : gatewayResponse.getResponseText());
246                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String
247                         .format("Failed obtaining pre-shared key with status code '%s'", gatewayResponse.getCode()));
248             }
249         } catch (URISyntaxException e) {
250             logger.error("Illegal gateway URI '{}'", authUrl, e);
251             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
252         } catch (JsonParseException e) {
253             logger.warn("Invalid response received from gateway '{}'", responseText, e);
254             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
255                     String.format("Invalid response received from gateway '%s'", responseText));
256         } catch (ConnectorException | IOException e) {
257             logger.debug("Error connecting to gateway ", e);
258             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
259                     String.format("Error connecting to gateway."));
260         }
261         return false;
262     }
263
264     @Override
265     public void dispose() {
266         if (scanJob != null) {
267             scanJob.cancel(true);
268             scanJob = null;
269         }
270         if (endPoint != null) {
271             endPoint.destroy();
272             endPoint = null;
273         }
274         if (deviceClient != null) {
275             deviceClient.shutdown();
276             deviceClient = null;
277         }
278         super.dispose();
279     }
280
281     /**
282      * Does a request to the gateway to list all available devices/services.
283      * The response is received and processed by the method {@link onUpdate(JsonElement data)}.
284      */
285     public void startScan() {
286         if (endPoint != null) {
287             requestGatewayInfo();
288             deviceClient.get(new TradfriCoapHandler(this));
289         }
290     }
291
292     /**
293      * Returns the root URI of the gateway.
294      *
295      * @return root URI of the gateway with coaps scheme
296      */
297     public String getGatewayURI() {
298         return gatewayURI;
299     }
300
301     /**
302      * Returns the coap endpoint that can be used within coap clients.
303      *
304      * @return the coap endpoint
305      */
306     public @Nullable CoapEndpoint getEndpoint() {
307         return endPoint;
308     }
309
310     @Override
311     public void onUpdate(JsonElement data) {
312         logger.debug("onUpdate response: {}", data);
313         if (endPoint != null) {
314             try {
315                 JsonArray array = data.getAsJsonArray();
316                 for (int i = 0; i < array.size(); i++) {
317                     requestDeviceDetails(array.get(i).getAsString());
318                 }
319             } catch (JsonSyntaxException e) {
320                 logger.debug("JSON error: {}", e.getMessage());
321                 setStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
322             }
323         }
324     }
325
326     private synchronized void requestGatewayInfo() {
327         // we are reusing our coap client and merely temporarily set a gateway info to call
328         deviceClient.setURI(gatewayInfoURI);
329         deviceClient.asyncGet().thenAccept(data -> {
330             logger.debug("requestGatewayInfo response: {}", data);
331             JsonObject json = JsonParser.parseString(data).getAsJsonObject();
332             String firmwareVersion = json.get(VERSION).getAsString();
333             getThing().setProperty(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
334             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
335         });
336         // restore root URI
337         deviceClient.setURI(gatewayURI);
338     }
339
340     private synchronized void requestDeviceDetails(String instanceId) {
341         // we are reusing our coap client and merely temporarily set a sub-URI to call
342         deviceClient.setURI(gatewayURI + "/" + instanceId);
343         deviceClient.asyncGet().thenAccept(data -> {
344             logger.debug("requestDeviceDetails response: {}", data);
345             JsonObject json = JsonParser.parseString(data).getAsJsonObject();
346             deviceUpdateListeners.forEach(listener -> listener.onUpdate(instanceId, json));
347         });
348         // restore root URI
349         deviceClient.setURI(gatewayURI);
350     }
351
352     @Override
353     public void setStatus(ThingStatus status, ThingStatusDetail statusDetail) {
354         // to fix connection issues after a gateway reboot, a session resume is forced for the next command
355         if (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR) {
356             logger.debug("Gateway communication error. Forcing a re-initialization!");
357             dispose();
358             initialize();
359         }
360
361         // are we still connected at all?
362         if (endPoint != null) {
363             updateStatus(status, statusDetail);
364         }
365     }
366
367     /**
368      * Registers a listener, which is informed about device details.
369      *
370      * @param listener the listener to register
371      */
372     public void registerDeviceUpdateListener(DeviceUpdateListener listener) {
373         this.deviceUpdateListeners.add(listener);
374     }
375
376     /**
377      * Unregisters a given listener.
378      *
379      * @param listener the listener to unregister
380      */
381     public void unregisterDeviceUpdateListener(DeviceUpdateListener listener) {
382         this.deviceUpdateListeners.remove(listener);
383     }
384
385     private boolean isNullOrEmpty(@Nullable String string) {
386         return string == null || string.isEmpty();
387     }
388
389     @Override
390     public void thingUpdated(Thing thing) {
391         super.thingUpdated(thing);
392
393         logger.info("Bridge configuration updated. Updating paired things (if any).");
394         for (Thing t : getThing().getThings()) {
395             final ThingHandler thingHandler = t.getHandler();
396             if (thingHandler != null) {
397                 thingHandler.thingUpdated(t);
398             }
399         }
400     }
401 }