]> git.basschouten.com Git - openhab-addons.git/blob
a6f3bdbe69a111836b88fd48412ee72b134f7155
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.bluetooth.govee.internal;
14
15 import java.util.UUID;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.Future;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledThreadPoolExecutor;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.TimeoutException;
22 import java.util.concurrent.locks.Condition;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
27 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
28 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
29 import org.openhab.binding.bluetooth.BluetoothDescriptor;
30 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
31 import org.openhab.binding.bluetooth.BluetoothService;
32 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
33 import org.openhab.core.common.NamedThreadFactory;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.util.HexUtils;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
43  *
44  * @author Kai Kreuzer - Initial contribution and API
45  * @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
46  */
47 @Deprecated
48 @NonNullByDefault
49 public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
50
51     private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
52
53     private final Condition connectionCondition = deviceLock.newCondition();
54     private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
55     private final Condition charCompleteCondition = deviceLock.newCondition();
56
57     private @Nullable Future<?> reconnectJob;
58     private @Nullable Future<?> pendingDisconnect;
59     private @Nullable BluetoothCharacteristic ongoingCharacteristic;
60     private @Nullable BluetoothCompletionStatus completeStatus;
61
62     private boolean connectOnDemand;
63     private int idleDisconnectDelayMs = 1000;
64
65     protected @Nullable ScheduledExecutorService connectionTaskExecutor;
66     private volatile boolean servicesDiscovered;
67
68     public ConnectedBluetoothHandler(Thing thing) {
69         super(thing);
70     }
71
72     @Override
73     public void initialize() {
74
75         // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
76         // the connectionTaskExecutor first
77         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
78                 new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true));
79         executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
80         executor.setRemoveOnCancelPolicy(true);
81         connectionTaskExecutor = executor;
82
83         super.initialize();
84
85         connectOnDemand = true;
86
87         Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
88         idleDisconnectDelayMs = 1000;
89         if (idleDisconnectDelayRaw instanceof Number) {
90             idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
91         }
92
93         if (!connectOnDemand) {
94             reconnectJob = executor.scheduleWithFixedDelay(() -> {
95                 try {
96                     if (device.getConnectionState() != ConnectionState.CONNECTED) {
97                         device.connect();
98                         // we do not set the Thing status here, because we will anyhow receive a call to
99                         // onConnectionStateChange
100                     } else {
101                         // just in case it was already connected to begin with
102                         updateStatus(ThingStatus.ONLINE);
103                         if (!servicesDiscovered && !device.discoverServices()) {
104                             logger.debug("Error while discovering services");
105                         }
106                     }
107                 } catch (RuntimeException ex) {
108                     logger.warn("Unexpected error occurred", ex);
109                 }
110             }, 0, 30, TimeUnit.SECONDS);
111         }
112     }
113
114     @Override
115     public void dispose() {
116         cancel(reconnectJob);
117         reconnectJob = null;
118         cancel(pendingDisconnect);
119         pendingDisconnect = null;
120
121         super.dispose();
122
123         shutdown(connectionTaskExecutor);
124         connectionTaskExecutor = null;
125     }
126
127     private static void cancel(@Nullable Future<?> future) {
128         if (future != null) {
129             future.cancel(true);
130         }
131     }
132
133     private void shutdown(@Nullable ScheduledExecutorService executor) {
134         if (executor != null) {
135             executor.shutdownNow();
136         }
137     }
138
139     private ScheduledExecutorService getConnectionTaskExecutor() {
140         var executor = connectionTaskExecutor;
141         if (executor == null) {
142             throw new IllegalStateException("characteristicScheduler has not been initialized");
143         }
144         return executor;
145     }
146
147     private void scheduleDisconnect() {
148         cancel(pendingDisconnect);
149         pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
150                 TimeUnit.MILLISECONDS);
151     }
152
153     private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
154         if (device.getConnectionState() == ConnectionState.CONNECTED) {
155             return;
156         }
157         if (device.getConnectionState() != ConnectionState.CONNECTING) {
158             if (!device.connect()) {
159                 throw new ConnectionException("Failed to start connecting");
160             }
161         }
162         logger.debug("waiting for connection");
163         if (!awaitConnection(1, TimeUnit.SECONDS)) {
164             throw new TimeoutException("Connection attempt timeout.");
165         }
166         logger.debug("connection successful");
167         if (!servicesDiscovered) {
168             logger.debug("discovering services");
169             device.discoverServices();
170             if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
171                 throw new TimeoutException("Service discovery timeout");
172             }
173             logger.debug("service discovery successful");
174         }
175     }
176
177     private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
178         deviceLock.lock();
179         try {
180             long nanosTimeout = unit.toNanos(timeout);
181             while (device.getConnectionState() != ConnectionState.CONNECTED) {
182                 if (nanosTimeout <= 0L) {
183                     return false;
184                 }
185                 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
186             }
187         } finally {
188             deviceLock.unlock();
189         }
190         return true;
191     }
192
193     private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
194         deviceLock.lock();
195         try {
196             long nanosTimeout = unit.toNanos(timeout);
197             while (ongoingCharacteristic != null) {
198                 if (nanosTimeout <= 0L) {
199                     return false;
200                 }
201                 nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
202             }
203         } finally {
204             deviceLock.unlock();
205         }
206         return true;
207     }
208
209     private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
210         deviceLock.lock();
211         try {
212             long nanosTimeout = unit.toNanos(timeout);
213             while (!servicesDiscovered) {
214                 if (nanosTimeout <= 0L) {
215                     return false;
216                 }
217                 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
218             }
219         } finally {
220             deviceLock.unlock();
221         }
222         return true;
223     }
224
225     private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
226             throws BluetoothException, TimeoutException, InterruptedException {
227         connectAndWait();
228         BluetoothService service = device.getServices(serviceUUID);
229         if (service == null) {
230             throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
231         }
232         BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
233         if (characteristic == null) {
234             throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
235         }
236         return characteristic;
237     }
238
239     private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
240             CallableFunction<BluetoothCharacteristic, T> callable) {
241         CompletableFuture<T> future = new CompletableFuture<>();
242         var executor = connectionTaskExecutor;
243         if (executor != null) {
244             executor.execute(() -> {
245                 cancel(pendingDisconnect);
246                 try {
247                     BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
248                             characteristicUUID);
249                     future.complete(callable.call(characteristic));
250                 } catch (InterruptedException e) {
251                     future.completeExceptionally(e);
252                     return;// we don't want to schedule anything if we receive an interrupt
253                 } catch (TimeoutException e) {
254                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
255                     future.completeExceptionally(e);
256                 } catch (Exception e) {
257                     future.completeExceptionally(e);
258                 }
259                 if (connectOnDemand) {
260                     scheduleDisconnect();
261                 }
262             });
263         } else {
264             future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
265         }
266         return future;
267     }
268
269     public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
270         return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
271             if (!device.enableNotifications(characteristic)) {
272                 throw new BluetoothException(
273                         "Failed to start notifications for characteristic: " + characteristic.getUuid());
274             }
275             return null;
276         });
277     }
278
279     public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
280             boolean enableNotification) {
281         return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
282             if (enableNotification) {
283                 if (!device.enableNotifications(characteristic)) {
284                     throw new BluetoothException(
285                             "Failed to start characteristic notification" + characteristic.getUuid());
286                 }
287             }
288             // now block for completion
289             characteristic.setValue(data);
290             ongoingCharacteristic = characteristic;
291             if (!device.writeCharacteristic(characteristic)) {
292                 throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
293             }
294             if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
295                 ongoingCharacteristic = null;
296                 throw new TimeoutException(
297                         "Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
298             }
299             if (completeStatus == BluetoothCompletionStatus.ERROR) {
300                 throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
301             }
302             logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
303                     characteristic.getUuid(), address);
304             return null;
305         });
306     }
307
308     public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
309         return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
310             // now block for completion
311             ongoingCharacteristic = characteristic;
312             if (!device.readCharacteristic(characteristic)) {
313                 throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
314             }
315             if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
316                 ongoingCharacteristic = null;
317                 throw new TimeoutException(
318                         "Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
319             }
320             if (completeStatus == BluetoothCompletionStatus.ERROR) {
321                 throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
322             }
323             byte[] data = characteristic.getByteValue();
324             logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
325                     HexUtils.bytesToHex(data));
326             return data;
327         });
328     }
329
330     @Override
331     protected void updateStatusBasedOnRssi(boolean receivedSignal) {
332         // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
333         // we are connected.
334         if (receivedSignal) {
335             if (device.getConnectionState() == ConnectionState.CONNECTED) {
336                 updateStatus(ThingStatus.ONLINE);
337             } else {
338                 if (!connectOnDemand) {
339                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
340                 }
341             }
342         } else {
343             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
344         }
345     }
346
347     @Override
348     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
349         super.onConnectionStateChange(connectionNotification);
350         switch (connectionNotification.getConnectionState()) {
351             case DISCOVERED:
352                 // The device is now known on the Bluetooth network, so we can do something...
353                 if (!connectOnDemand) {
354                     getConnectionTaskExecutor().submit(() -> {
355                         if (device.getConnectionState() != ConnectionState.CONNECTED) {
356                             if (!device.connect()) {
357                                 logger.debug("Error connecting to device after discovery.");
358                             }
359                         }
360                     });
361                 }
362                 break;
363             case CONNECTED:
364                 deviceLock.lock();
365                 try {
366                     connectionCondition.signal();
367                 } finally {
368                     deviceLock.unlock();
369                 }
370                 if (!connectOnDemand) {
371                     getConnectionTaskExecutor().submit(() -> {
372                         if (!servicesDiscovered && !device.discoverServices()) {
373                             logger.debug("Error while discovering services");
374                         }
375                     });
376                 }
377                 break;
378             case DISCONNECTED:
379                 var future = pendingDisconnect;
380                 if (future != null) {
381                     future.cancel(false);
382                 }
383                 if (!connectOnDemand) {
384                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
385                 }
386                 break;
387             default:
388                 break;
389         }
390     }
391
392     @Override
393     public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
394         super.onCharacteristicReadComplete(characteristic, status);
395         deviceLock.lock();
396         try {
397             if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
398                 completeStatus = status;
399                 ongoingCharacteristic = null;
400                 charCompleteCondition.signal();
401             }
402         } finally {
403             deviceLock.unlock();
404         }
405     }
406
407     @Override
408     public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
409             BluetoothCompletionStatus status) {
410         super.onCharacteristicWriteComplete(characteristic, status);
411         deviceLock.lock();
412         try {
413             if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
414                 completeStatus = status;
415                 ongoingCharacteristic = null;
416                 charCompleteCondition.signal();
417             }
418         } finally {
419             deviceLock.unlock();
420         }
421     }
422
423     @Override
424     public void onServicesDiscovered() {
425         super.onServicesDiscovered();
426         deviceLock.lock();
427         try {
428             this.servicesDiscovered = true;
429             serviceDiscoveryCondition.signal();
430         } finally {
431             deviceLock.unlock();
432         }
433         logger.debug("Service discovery completed for '{}'", address);
434     }
435
436     @Override
437     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
438         super.onCharacteristicUpdate(characteristic);
439         if (logger.isDebugEnabled()) {
440             logger.debug("Recieved update {} to characteristic {} of device {}",
441                     HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
442         }
443     }
444
445     @Override
446     public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
447         super.onDescriptorUpdate(descriptor);
448         if (logger.isDebugEnabled()) {
449             logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
450                     descriptor.getUuid(), address);
451         }
452     }
453
454     public static class BluetoothException extends Exception {
455
456         public BluetoothException(String message) {
457             super(message);
458         }
459     }
460
461     public static class ConnectionException extends BluetoothException {
462
463         public ConnectionException(String message) {
464             super(message);
465         }
466     }
467
468     @FunctionalInterface
469     public static interface CallableFunction<U, R> {
470         public R call(U arg) throws Exception;
471     }
472 }