]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth] Changed characteristic read/write to use CompletableFutures (#8970)
authorConnor Petty <mistercpp2000@gmail.com>
Fri, 9 Apr 2021 20:23:28 +0000 (13:23 -0700)
committerGitHub <noreply@github.com>
Fri, 9 Apr 2021 20:23:28 +0000 (22:23 +0200)
Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
24 files changed:
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java
bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java
bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java
bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java
bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml
bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java [deleted file]
bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java
bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java
bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java

index f31aa533676f8f2eb65c010c8c72f5ba685abc91..4cac3551856c4d4b618cd8dda7fc063d76aaf91d 100644 (file)
@@ -24,8 +24,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
+import org.openhab.binding.bluetooth.BluetoothUtils;
 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.unit.SIUnits;
@@ -156,8 +156,35 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
                     case IDLE:
                         logger.debug("Read data from device {}...", address);
                         BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
-                        if (characteristic != null && device.readCharacteristic(characteristic)) {
+
+                        if (characteristic != null) {
                             readState = ReadState.READING;
+                            device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
+                                try {
+                                    if (data != null) {
+                                        logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
+                                                address, data);
+                                        updateStatus(ThingStatus.ONLINE);
+                                        sinceLastReadSec.set(0);
+                                        try {
+                                            updateChannels(
+                                                    new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data)));
+                                        } catch (AirthingsParserException e) {
+                                            logger.warn(
+                                                    "Data parsing error occured, when parsing data from device {}, cause {}",
+                                                    address, e.getMessage(), e);
+                                        }
+                                    } else {
+                                        logger.debug("Characteristic {} from device {} failed: {}",
+                                                characteristic.getUuid(), address, ex.getMessage());
+                                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                                ex.getMessage());
+                                    }
+                                } finally {
+                                    readState = ReadState.IDLE;
+                                    disconnect();
+                                }
+                            });
                         } else {
                             logger.debug("Read data from device {} failed", address);
                             disconnect();
@@ -205,30 +232,6 @@ public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
         execute();
     }
 
-    @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        try {
-            if (status == BluetoothCompletionStatus.SUCCESS) {
-                logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address,
-                        characteristic.getValue());
-                updateStatus(ThingStatus.ONLINE);
-                sinceLastReadSec.set(0);
-                try {
-                    updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue()));
-                } catch (AirthingsParserException e) {
-                    logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address,
-                            e.getMessage(), e);
-                }
-            } else {
-                logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device");
-            }
-        } finally {
-            readState = ReadState.IDLE;
-            disconnect();
-        }
-    }
-
     private void updateChannels(AirthingsWavePlusDataParser parser) {
         logger.debug("Parsed data: {}", parser);
         updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));
index 2588dd0620eac29f4363a942495e0a6f306ca48e..c0be052e3c5ee02f6c10f67b4afb51c5981f5e16 100644 (file)
@@ -12,7 +12,6 @@
  */
 package org.openhab.binding.bluetooth.am43.internal;
 
-import java.util.Arrays;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -24,7 +23,6 @@ import javax.measure.quantity.Length;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
 import org.openhab.binding.bluetooth.am43.internal.command.AM43Command;
@@ -164,7 +162,7 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
                 command.setState(AM43Command.State.FAILED);
                 return;
             }
-            if (!resolved) {
+            if (!device.isServicesDiscovered()) {
                 logger.debug("Unable to send command {} to device {}: services not resolved", command,
                         device.getAddress());
                 command.setState(AM43Command.State.FAILED);
@@ -180,9 +178,15 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
             // there is no consequence to calling this as much as we like
             device.enableNotifications(characteristic);
 
-            characteristic.setValue(command.getRequest());
             command.setState(AM43Command.State.ENQUEUED);
-            device.writeCharacteristic(characteristic);
+            device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> {
+                if (t != null) {
+                    logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage());
+                    command.setState(AM43Command.State.FAILED);
+                } else {
+                    command.setState(AM43Command.State.SENT);
+                }
+            });
 
             if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS,
                     AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) {
@@ -197,39 +201,8 @@ public class AM43Handler extends ConnectedBluetoothHandler implements ResponseLi
     }
 
     @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-        super.onCharacteristicWriteComplete(characteristic, status);
-
-        byte[] request = characteristic.getByteValue();
-
-        AM43Command command = currentCommand;
-
-        if (command != null) {
-            if (!Arrays.equals(request, command.getRequest())) {
-                logger.debug("Write completed for unknown command");
-                return;
-            }
-            switch (status) {
-                case SUCCESS:
-                    command.setState(AM43Command.State.SENT);
-                    break;
-                case ERROR:
-                    command.setState(AM43Command.State.FAILED);
-                    break;
-            }
-        } else {
-            if (logger.isDebugEnabled()) {
-                logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
-            }
-        }
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-        super.onCharacteristicUpdate(characteristic);
-
-        byte[] response = characteristic.getByteValue();
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) {
+        super.onCharacteristicUpdate(characteristic, response);
 
         AM43Command command = currentCommand;
         if (command == null) {
index ccff259a0b8785213c8bafa566e5b49c3f4e8b8d..3fce5a0e451db41f998a5e69c9df9308a3e4dbe5 100644 (file)
@@ -19,9 +19,11 @@ import java.util.Map;
 import java.util.NavigableMap;
 import java.util.TreeMap;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -29,10 +31,11 @@ import org.openhab.binding.bluetooth.BaseBluetoothDevice;
 import org.openhab.binding.bluetooth.BluetoothAddress;
 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDescriptor;
 import org.openhab.binding.bluetooth.BluetoothDevice;
+import org.openhab.binding.bluetooth.BluetoothException;
 import org.openhab.binding.bluetooth.BluetoothService;
+import org.openhab.binding.bluetooth.BluetoothUtils;
 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
@@ -66,6 +69,14 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
 
     private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
 
+    private static final BlueGigaProcedure PROCEDURE_NONE = new BlueGigaProcedure(BlueGigaProcedure.Type.NONE);
+    private static final BlueGigaProcedure PROCEDURE_GET_SERVICES = new BlueGigaProcedure(
+            BlueGigaProcedure.Type.GET_SERVICES);
+    private static final BlueGigaProcedure PROCEDURE_GET_CHARACTERISTICS = new BlueGigaProcedure(
+            BlueGigaProcedure.Type.GET_CHARACTERISTICS);
+    private static final BlueGigaProcedure PROCEDURE_READ_CHARACTERISTIC_DECL = new BlueGigaProcedure(
+            BlueGigaProcedure.Type.READ_CHARACTERISTIC_DECL);
+
     private Map<Integer, UUID> handleToUUID = new HashMap<>();
     private NavigableMap<Integer, BlueGigaBluetoothCharacteristic> handleToCharacteristic = new TreeMap<>();
 
@@ -75,22 +86,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     // The dongle handler
     private final BlueGigaBridgeHandler bgHandler;
 
-    // An enum to use in the state machine for interacting with the device
-    private enum BlueGigaProcedure {
-        NONE,
-        GET_SERVICES,
-        GET_CHARACTERISTICS,
-        READ_CHARACTERISTIC_DECL,
-        CHARACTERISTIC_READ,
-        CHARACTERISTIC_WRITE,
-        NOTIFICATION_ENABLE,
-        NOTIFICATION_DISABLE
-    }
-
-    private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
-
-    // Somewhere to remember what characteristic we're working on
-    private @Nullable BluetoothCharacteristic procedureCharacteristic;
+    private BlueGigaProcedure currentProcedure = PROCEDURE_NONE;
 
     // The connection handle if the device is connected
     private int connection = -1;
@@ -113,9 +109,24 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     private Runnable procedureTimeoutTask = new Runnable() {
         @Override
         public void run() {
-            logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
-            procedureProgress = BlueGigaProcedure.NONE;
-            procedureCharacteristic = null;
+            BlueGigaProcedure procedure = currentProcedure;
+            logger.debug("Procedure {} timeout for device {}", procedure.type, address);
+            switch (procedure.type) {
+                case CHARACTERISTIC_READ:
+                    ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
+                    readProcedure.readFuture.completeExceptionally(new TimeoutException("Read characteristic "
+                            + readProcedure.characteristic.getUuid() + " timeout for device " + address));
+                    break;
+                case CHARACTERISTIC_WRITE:
+                    WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
+                    writeProcedure.writeFuture.completeExceptionally(new TimeoutException("Write characteristic "
+                            + writeProcedure.characteristic.getUuid() + " timeout for device " + address));
+                    break;
+                default:
+                    break;
+            }
+
+            currentProcedure = PROCEDURE_NONE;
         }
     };
 
@@ -174,7 +185,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
 
     @Override
     public boolean discoverServices() {
-        if (procedureProgress != BlueGigaProcedure.NONE) {
+        if (currentProcedure != PROCEDURE_NONE) {
             return false;
         }
 
@@ -184,49 +195,45 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         }
 
         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-        procedureProgress = BlueGigaProcedure.GET_SERVICES;
+        currentProcedure = PROCEDURE_GET_SERVICES;
         return true;
     }
 
     @Override
-    public boolean enableNotifications(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
         if (connection == -1) {
-            logger.debug("Cannot enable notifications, device not connected {}", this);
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
         }
 
         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
         if (ch.isNotifying()) {
-            return true;
+            return CompletableFuture.completedFuture(null);
         }
 
         BluetoothDescriptor descriptor = ch
                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
 
         if (descriptor == null || descriptor.getHandle() == 0) {
-            logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
-            return false;
+            return CompletableFuture.failedFuture(
+                    new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
         }
 
-        if (procedureProgress != BlueGigaProcedure.NONE) {
-            logger.debug("Procedure already in progress {}", procedureProgress);
-            return false;
+        if (currentProcedure != PROCEDURE_NONE) {
+            return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
         }
 
         int[] value = { 1, 0 };
-        byte[] bvalue = toBytes(value);
-        descriptor.setValue(bvalue);
 
         cancelTimer(procedureTimer);
         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
-            logger.debug("bgWriteCharacteristic returned false");
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException(
+                    "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
         }
 
         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-        procedureProgress = BlueGigaProcedure.NOTIFICATION_ENABLE;
-        procedureCharacteristic = characteristic;
-
+        WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
+                BlueGigaProcedure.Type.NOTIFICATION_ENABLE);
+        currentProcedure = notifyProcedure;
         try {
             // we intentionally sleep here in order to give this procedure a chance to complete.
             // ideally we would use locks/conditions to make this wait until completiong but
@@ -235,57 +242,46 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
-        return true;
+        return notifyProcedure.writeFuture;
     }
 
     @Override
-    public boolean disableNotifications(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
         if (connection == -1) {
-            logger.debug("Cannot disable notifications, device not connected {}", this);
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
         }
 
         BlueGigaBluetoothCharacteristic ch = (BlueGigaBluetoothCharacteristic) characteristic;
         if (!ch.isNotifying()) {
-            return true;
+            return CompletableFuture.completedFuture(null);
         }
 
         BluetoothDescriptor descriptor = ch
                 .getDescriptor(BluetoothDescriptor.GattDescriptor.CLIENT_CHARACTERISTIC_CONFIGURATION.getUUID());
 
         if (descriptor == null || descriptor.getHandle() == 0) {
-            logger.debug("unable to find CCC for characteristic {}", characteristic.getUuid());
-            return false;
+            return CompletableFuture.failedFuture(
+                    new BluetoothException("Unable to find CCC for characteristic [" + characteristic.getUuid() + "]"));
         }
 
-        if (procedureProgress != BlueGigaProcedure.NONE) {
-            logger.debug("Procedure already in progress {}", procedureProgress);
-            return false;
+        if (currentProcedure != PROCEDURE_NONE) {
+            return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
         }
 
         int[] value = { 0, 0 };
-        byte[] bvalue = toBytes(value);
-        descriptor.setValue(bvalue);
 
         cancelTimer(procedureTimer);
         if (!bgHandler.bgWriteCharacteristic(connection, descriptor.getHandle(), value)) {
-            logger.debug("bgWriteCharacteristic returned false");
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException(
+                    "Failed to write to CCC for characteristic [" + characteristic.getUuid() + "]"));
         }
 
         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-        procedureProgress = BlueGigaProcedure.NOTIFICATION_DISABLE;
-        procedureCharacteristic = characteristic;
+        WriteCharacteristicProcedure notifyProcedure = new WriteCharacteristicProcedure(ch,
+                BlueGigaProcedure.Type.NOTIFICATION_DISABLE);
+        currentProcedure = notifyProcedure;
 
-        try {
-            // we intentionally sleep here in order to give this procedure a chance to complete.
-            // ideally we would use locks/conditions to make this wait until completiong but
-            // I have a better solution planned for later. - Connor Petty
-            Thread.sleep(500);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        }
-        return true;
+        return notifyProcedure.writeFuture;
     }
 
     @Override
@@ -307,52 +303,56 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     }
 
     @Override
-    public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
-        if (characteristic == null || characteristic.getHandle() == 0) {
-            return false;
+    public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
+        if (characteristic.getHandle() == 0) {
+            return CompletableFuture.failedFuture(new BluetoothException("Cannot read characteristic with no handle"));
         }
         if (connection == -1) {
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
         }
 
-        if (procedureProgress != BlueGigaProcedure.NONE) {
-            return false;
+        if (currentProcedure != PROCEDURE_NONE) {
+            return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
         }
 
         cancelTimer(procedureTimer);
         if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
-            return false;
+            return CompletableFuture.failedFuture(
+                    new BluetoothException("Failed to read characteristic [" + characteristic.getUuid() + "]"));
         }
         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-        procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
-        procedureCharacteristic = characteristic;
+        ReadCharacteristicProcedure readProcedure = new ReadCharacteristicProcedure(characteristic);
+        currentProcedure = readProcedure;
 
-        return true;
+        return readProcedure.readFuture;
     }
 
     @Override
-    public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
-        if (characteristic == null || characteristic.getHandle() == 0) {
-            return false;
+    public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
+        if (characteristic.getHandle() == 0) {
+            return CompletableFuture.failedFuture(new BluetoothException("Cannot write characteristic with no handle"));
         }
         if (connection == -1) {
-            return false;
+            return CompletableFuture.failedFuture(new BluetoothException("Not connected"));
         }
 
-        if (procedureProgress != BlueGigaProcedure.NONE) {
-            return false;
+        if (currentProcedure != PROCEDURE_NONE) {
+            return CompletableFuture.failedFuture(new BluetoothException("Another procedure is already in progress"));
         }
 
         cancelTimer(procedureTimer);
-        if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
-            return false;
+        if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(),
+                BluetoothUtils.toIntArray(value))) {
+            return CompletableFuture.failedFuture(
+                    new BluetoothException("Failed to write characteristic [" + characteristic.getUuid() + "]"));
         }
 
         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-        procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
-        procedureCharacteristic = characteristic;
+        WriteCharacteristicProcedure writeProcedure = new WriteCharacteristicProcedure(
+                (BlueGigaBluetoothCharacteristic) characteristic, BlueGigaProcedure.Type.CHARACTERISTIC_WRITE);
+        currentProcedure = writeProcedure;
 
-        return true;
+        return writeProcedure.writeFuture;
     }
 
     @Override
@@ -564,7 +564,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
             return;
         }
 
-        if (procedureProgress == BlueGigaProcedure.NONE) {
+        if (currentProcedure == PROCEDURE_NONE) {
             logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
                     connection, address);
             return;
@@ -574,63 +574,73 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         updateLastSeenTime();
 
         // The current procedure is now complete - move on...
-        switch (procedureProgress) {
+        switch (currentProcedure.type) {
             case GET_SERVICES:
                 // We've downloaded all services, now get the characteristics
                 if (bgHandler.bgFindCharacteristics(connection)) {
                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-                    procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
+                    currentProcedure = PROCEDURE_GET_CHARACTERISTICS;
                 } else {
-                    procedureProgress = BlueGigaProcedure.NONE;
+                    currentProcedure = PROCEDURE_NONE;
                 }
                 break;
             case GET_CHARACTERISTICS:
                 // We've downloaded all attributes, now read the characteristic declarations
                 if (bgHandler.bgReadCharacteristicDeclarations(connection)) {
                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
-                    procedureProgress = BlueGigaProcedure.READ_CHARACTERISTIC_DECL;
+                    currentProcedure = PROCEDURE_READ_CHARACTERISTIC_DECL;
                 } else {
-                    procedureProgress = BlueGigaProcedure.NONE;
+                    currentProcedure = PROCEDURE_NONE;
                 }
                 break;
             case READ_CHARACTERISTIC_DECL:
                 // We've downloaded read all the declarations, we are done now
-                procedureProgress = BlueGigaProcedure.NONE;
+                currentProcedure = PROCEDURE_NONE;
                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
                 break;
             case CHARACTERISTIC_READ:
                 // The read failed
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
-                        BluetoothCompletionStatus.ERROR);
-                procedureProgress = BlueGigaProcedure.NONE;
-                procedureCharacteristic = null;
+                ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
+                readProcedure.readFuture.completeExceptionally(new BluetoothException(
+                        "Read characteristic failed: " + readProcedure.characteristic.getUuid()));
+                currentProcedure = PROCEDURE_NONE;
                 break;
             case CHARACTERISTIC_WRITE:
                 // The write completed - failure or success
-                BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
-                        ? BluetoothCompletionStatus.SUCCESS
-                        : BluetoothCompletionStatus.ERROR;
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
-                procedureProgress = BlueGigaProcedure.NONE;
-                procedureCharacteristic = null;
+                WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) currentProcedure;
+                if (event.getResult() == BgApiResponse.SUCCESS) {
+                    writeProcedure.writeFuture.complete(null);
+                } else {
+                    writeProcedure.writeFuture.completeExceptionally(new BluetoothException(
+                            "Write characteristic failed: " + writeProcedure.characteristic.getUuid()));
+                }
+                currentProcedure = PROCEDURE_NONE;
                 break;
             case NOTIFICATION_ENABLE:
+                WriteCharacteristicProcedure notifyEnableProcedure = (WriteCharacteristicProcedure) currentProcedure;
                 boolean success = event.getResult() == BgApiResponse.SUCCESS;
-                if (!success) {
-                    logger.debug("write to descriptor failed");
+                if (success) {
+                    notifyEnableProcedure.writeFuture.complete(null);
+                } else {
+                    notifyEnableProcedure.writeFuture
+                            .completeExceptionally(new BluetoothException("Enable characteristic notification failed: "
+                                    + notifyEnableProcedure.characteristic.getUuid()));
                 }
-                ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(success);
-                procedureProgress = BlueGigaProcedure.NONE;
-                procedureCharacteristic = null;
+                notifyEnableProcedure.characteristic.setNotifying(success);
+                currentProcedure = PROCEDURE_NONE;
                 break;
             case NOTIFICATION_DISABLE:
+                WriteCharacteristicProcedure notifyDisableProcedure = (WriteCharacteristicProcedure) currentProcedure;
                 success = event.getResult() == BgApiResponse.SUCCESS;
-                if (!success) {
-                    logger.debug("write to descriptor failed");
+                if (success) {
+                    notifyDisableProcedure.writeFuture.complete(null);
+                } else {
+                    notifyDisableProcedure.writeFuture
+                            .completeExceptionally(new BluetoothException("Disable characteristic notification failed: "
+                                    + notifyDisableProcedure.characteristic.getUuid()));
                 }
-                ((BlueGigaBluetoothCharacteristic) procedureCharacteristic).setNotifying(!success);
-                procedureProgress = BlueGigaProcedure.NONE;
-                procedureCharacteristic = null;
+                notifyDisableProcedure.characteristic.setNotifying(!success);
+                currentProcedure = PROCEDURE_NONE;
                 break;
             default:
                 break;
@@ -668,7 +678,23 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         cancelTimer(procedureTimer);
         connectionState = ConnectionState.DISCONNECTED;
         connection = -1;
-        procedureProgress = BlueGigaProcedure.NONE;
+
+        BlueGigaProcedure procedure = currentProcedure;
+        switch (procedure.type) {
+            case CHARACTERISTIC_READ:
+                ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) procedure;
+                readProcedure.readFuture.completeExceptionally(new BluetoothException("Read characteristic "
+                        + readProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
+                break;
+            case CHARACTERISTIC_WRITE:
+                WriteCharacteristicProcedure writeProcedure = (WriteCharacteristicProcedure) procedure;
+                writeProcedure.writeFuture.completeExceptionally(new BluetoothException("Write characteristic "
+                        + writeProcedure.characteristic.getUuid() + " failed due to disconnect of device " + address));
+                break;
+            default:
+                break;
+        }
+        currentProcedure = PROCEDURE_NONE;
 
         notifyListeners(BluetoothEventType.CONNECTION_STATE,
                 new BluetoothConnectionStatusNotification(connectionState));
@@ -707,42 +733,30 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
             return;
         }
         if (handle == characteristic.getHandle()) {
-            characteristic.setValue(event.getValue().clone());
-
+            byte[] value = BluetoothUtils.toByteArray(event.getValue());
+            BlueGigaProcedure procedure = currentProcedure;
             // If this is the characteristic we were reading, then send a read completion
-            if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
-                    && procedureCharacteristic.getHandle() == event.getAttHandle()) {
-                procedureProgress = BlueGigaProcedure.NONE;
-                procedureCharacteristic = null;
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
-                        BluetoothCompletionStatus.SUCCESS);
-                return;
+            if (procedure.type == BlueGigaProcedure.Type.CHARACTERISTIC_READ) {
+                ReadCharacteristicProcedure readProcedure = (ReadCharacteristicProcedure) currentProcedure;
+                if (readProcedure.characteristic.getHandle() == event.getAttHandle()) {
+                    readProcedure.readFuture.complete(value);
+                    currentProcedure = PROCEDURE_NONE;
+                    return;
+                }
             }
-
             // Notify the user of the updated value
-            notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
+            notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
         } else {
             // it must be one of the descriptors we need to update
             UUID attUUID = handleToUUID.get(handle);
             BluetoothDescriptor descriptor = characteristic.getDescriptor(attUUID);
-            descriptor.setValue(toBytes(event.getValue()));
-            notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
-        }
-    }
-
-    private static byte @Nullable [] toBytes(int @Nullable [] value) {
-        if (value == null) {
-            return null;
+            notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor,
+                    BluetoothUtils.toByteArray(event.getValue()));
         }
-        byte[] ret = new byte[value.length];
-        for (int i = 0; i < value.length; i++) {
-            ret[i] = (byte) value[i];
-        }
-        return ret;
     }
 
     private boolean parseDeclaration(BlueGigaBluetoothCharacteristic ch, int[] value) {
-        ByteBuffer buffer = ByteBuffer.wrap(toBytes(value));
+        ByteBuffer buffer = ByteBuffer.wrap(BluetoothUtils.toByteArray(value));
         buffer.order(ByteOrder.LITTLE_ENDIAN);
 
         ch.setProperties(Byte.toUnsignedInt(buffer.get()));
@@ -779,7 +793,7 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
         cancelTimer(connectTimer);
         cancelTimer(procedureTimer);
         bgHandler.removeEventListener(this);
-        procedureProgress = BlueGigaProcedure.NONE;
+        currentProcedure = PROCEDURE_NONE;
         connectionState = ConnectionState.DISCOVERING;
         connection = -1;
     }
@@ -793,4 +807,48 @@ public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements Blue
     private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
         return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
     }
+
+    private static class BlueGigaProcedure {
+        private final Type type;
+
+        public BlueGigaProcedure(Type type) {
+            this.type = type;
+        }
+
+        // An enum to use in the state machine for interacting with the device
+        enum Type {
+            NONE,
+            GET_SERVICES,
+            GET_CHARACTERISTICS,
+            READ_CHARACTERISTIC_DECL,
+            CHARACTERISTIC_READ,
+            CHARACTERISTIC_WRITE,
+            NOTIFICATION_ENABLE,
+            NOTIFICATION_DISABLE
+        }
+    }
+
+    private static class ReadCharacteristicProcedure extends BlueGigaProcedure {
+
+        private final BluetoothCharacteristic characteristic;
+
+        private final CompletableFuture<byte[]> readFuture = new CompletableFuture<>();
+
+        public ReadCharacteristicProcedure(BluetoothCharacteristic characteristic) {
+            super(Type.CHARACTERISTIC_READ);
+            this.characteristic = characteristic;
+        }
+    }
+
+    private static class WriteCharacteristicProcedure extends BlueGigaProcedure {
+
+        private final BlueGigaBluetoothCharacteristic characteristic;
+
+        private final CompletableFuture<@Nullable Void> writeFuture = new CompletableFuture<>();
+
+        public WriteCharacteristicProcedure(BlueGigaBluetoothCharacteristic characteristic, Type type) {
+            super(type);
+            this.characteristic = characteristic;
+        }
+    }
 }
index 509f21fa429a1939d28fc8a5b4c1812e1cec3576..4f865323a26d465ea7ba4fcbfba1ef43771db4e5 100644 (file)
@@ -15,10 +15,10 @@ package org.openhab.binding.bluetooth.bluez.internal;
 import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
-import org.bluez.exceptions.BluezFailedException;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.freedesktop.dbus.errors.NoReply;
@@ -28,7 +28,6 @@ import org.freedesktop.dbus.types.UInt16;
 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
 import org.openhab.binding.bluetooth.BluetoothAddress;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDescriptor;
 import org.openhab.binding.bluetooth.BluetoothService;
 import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
@@ -42,6 +41,8 @@ import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent
 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
+import org.openhab.binding.bluetooth.util.RetryException;
+import org.openhab.binding.bluetooth.util.RetryFuture;
 import org.openhab.core.common.ThreadPoolManager;
 import org.openhab.core.util.HexUtils;
 import org.slf4j.Logger;
@@ -257,57 +258,65 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
     }
 
     @Override
-    public boolean enableNotifications(BluetoothCharacteristic characteristic) {
-        ensureConnected();
+    public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
+        BluetoothDevice dev = device;
+        if (dev == null || !dev.isConnected()) {
+            return CompletableFuture
+                    .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
+        }
 
         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
-        if (c != null) {
+        if (c == null) {
+            logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
+            return CompletableFuture.failedFuture(
+                    new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
+        }
 
+        return RetryFuture.callWithRetry(() -> {
             try {
                 c.startNotify();
             } catch (DBusException e) {
                 if (e.getMessage().contains("Already notifying")) {
-                    return false;
+                    return null;
                 } else if (e.getMessage().contains("In Progress")) {
-                    // let's retry in 10 seconds
-                    scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
+                    // let's retry in half a second
+                    throw new RetryException(500, TimeUnit.MILLISECONDS);
                 } else {
                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
+                    throw e;
                 }
             }
-            return true;
-        } else {
-            logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
-            return false;
-        }
+            return null;
+        }, scheduler);
     }
 
     @Override
-    public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
         logger.debug("writeCharacteristic()");
 
-        ensureConnected();
+        BluetoothDevice dev = device;
+        if (dev == null || !dev.isConnected()) {
+            return CompletableFuture
+                    .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
+        }
 
         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
         if (c == null) {
             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
-            return false;
+            return CompletableFuture.failedFuture(
+                    new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
         }
 
-        scheduler.submit(() -> {
+        return RetryFuture.callWithRetry(() -> {
             try {
-                c.writeValue(characteristic.getByteValue(), null);
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
-                        BluetoothCompletionStatus.SUCCESS);
-
+                c.writeValue(value, null);
+                return null;
             } catch (DBusException e) {
                 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
                         characteristic.getUuid(), e.getMessage());
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
-                        BluetoothCompletionStatus.ERROR);
+                throw e;
             }
-        });
-        return true;
+        }, scheduler);
     }
 
     @Override
@@ -364,10 +373,7 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
         }
         BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
         if (c != null) {
-            synchronized (c) {
-                c.setValue(event.getData());
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
-            }
+            notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
         }
     }
 
@@ -453,28 +459,30 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
     }
 
     @Override
-    public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
+        BluetoothDevice dev = device;
+        if (dev == null || !dev.isConnected()) {
+            return CompletableFuture
+                    .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
+        }
+
         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
         if (c == null) {
             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
-            return false;
+            return CompletableFuture.failedFuture(
+                    new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
         }
 
-        scheduler.submit(() -> {
+        return RetryFuture.callWithRetry(() -> {
             try {
-                byte[] value = c.readValue(null);
-                characteristic.setValue(value);
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
-                        BluetoothCompletionStatus.SUCCESS);
+                return c.readValue(null);
             } catch (DBusException | DBusExecutionException e) {
                 // DBusExecutionException is thrown if the value cannot be read
                 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
                         e.getMessage());
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
-                        BluetoothCompletionStatus.ERROR);
+                throw e;
             }
-        });
-        return true;
+        }, scheduler);
     }
 
     @Override
@@ -490,24 +498,35 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
     }
 
     @Override
-    public boolean disableNotifications(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
+        BluetoothDevice dev = device;
+        if (dev == null || !dev.isConnected()) {
+            return CompletableFuture
+                    .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
+        }
         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
-        if (c != null) {
+        if (c == null) {
+            logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
+            return CompletableFuture.failedFuture(
+                    new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
+        }
+
+        return RetryFuture.callWithRetry(() -> {
             try {
                 c.stopNotify();
-            } catch (BluezFailedException e) {
-                if (e.getMessage().contains("In Progress")) {
-                    // let's retry in 10 seconds
-                    scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
+            } catch (DBusException e) {
+                if (e.getMessage().contains("Already notifying")) {
+                    return null;
+                } else if (e.getMessage().contains("In Progress")) {
+                    // let's retry in half a second
+                    throw new RetryException(500, TimeUnit.MILLISECONDS);
                 } else {
-                    logger.warn("Exception occurred while activating notifications on '{}'", address, e);
+                    logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
+                    throw e;
                 }
             }
-            return true;
-        } else {
-            logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
-            return false;
-        }
+            return null;
+        }, scheduler);
     }
 
     @Override
index e8169113b7beef79cc78d6364be500a82f30a8c3..d1a2ba510010c997e0a8c7f61a04e3d8186f3b65 100644 (file)
  */
 package org.openhab.binding.bluetooth.daikinmadoka.handler;
 
-import java.util.Arrays;
 import java.util.Random;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import javax.measure.quantity.Temperature;
 import javax.measure.quantity.Time;
@@ -27,7 +28,6 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
 import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants;
@@ -350,12 +350,12 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
     }
 
     @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] msgBytes) {
         if (logger.isDebugEnabled()) {
             logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(),
-                    HexUtils.bytesToHex(characteristic.getByteValue()));
+                    HexUtils.bytesToHex(msgBytes));
         }
-        super.onCharacteristicUpdate(characteristic);
+        super.onCharacteristicUpdate(characteristic, msgBytes);
 
         // Check that arguments are valid.
         if (characteristic.getUuid() == null) {
@@ -367,9 +367,8 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
             return;
         }
 
-        // A message cannot be null or have a 0-byte length
-        byte[] msgBytes = characteristic.getByteValue();
-        if (msgBytes == null || msgBytes.length == 0) {
+        // A message cannot have a 0-byte length
+        if (msgBytes.length == 0) {
             return;
         }
 
@@ -398,7 +397,7 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
                 return;
             }
 
-            if (!resolved) {
+            if (!device.isServicesDiscovered()) {
                 logger.debug("Unable to send command {} to device {}: services not resolved",
                         command.getClass().getSimpleName(), device.getAddress());
                 command.setState(BRC1HCommand.State.FAILED);
@@ -424,17 +423,23 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
 
             // Commands can be composed of multiple chunks
             for (byte[] chunk : command.getRequest()) {
-                charWrite.setValue(chunk);
                 command.setState(BRC1HCommand.State.ENQUEUED);
                 for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
-                    if (device.writeCharacteristic(charWrite)) {
-                        command.setState(BRC1HCommand.State.SENT);
-                        synchronized (command) {
-                            command.wait(100);
-                        }
-                        break;
+                    try {
+                        device.writeCharacteristic(charWrite, chunk).get(100, TimeUnit.MILLISECONDS);
+                    } catch (InterruptedException ex) {
+                        return;
+                    } catch (ExecutionException ex) {
+                        logger.debug("Error while writing message {}: {}", command.getClass().getSimpleName(),
+                                ex.getMessage());
+                        Thread.sleep(100);
+                        continue;
+                    } catch (TimeoutException ex) {
+                        Thread.sleep(100);
+                        continue;
                     }
-                    Thread.sleep(100);
+                    command.setState(BRC1HCommand.State.SENT);
+                    break;
                 }
             }
 
@@ -459,39 +464,6 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re
         }
     }
 
-    @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-        super.onCharacteristicWriteComplete(characteristic, status);
-
-        byte[] request = characteristic.getByteValue();
-        BRC1HCommand command = currentCommand;
-
-        if (command != null) {
-            // last chunk:
-            byte[] lastChunk = command.getRequest()[command.getRequest().length - 1];
-            if (!Arrays.equals(request, lastChunk)) {
-                logger.debug("Write completed for a chunk, but not a complete command.");
-                synchronized (command) {
-                    command.notify();
-                }
-                return;
-            }
-            switch (status) {
-                case SUCCESS:
-                    command.setState(BRC1HCommand.State.SENT);
-                    break;
-                case ERROR:
-                    command.setState(BRC1HCommand.State.FAILED);
-                    break;
-            }
-        } else {
-            if (logger.isDebugEnabled()) {
-                logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
-            }
-        }
-    }
-
     /**
      * When the method is triggered, it means that all message chunks have been received, re-assembled in the right
      * order and that the payload is ready to be processed.
index 633170f4284e4e2a133f7cc478fb3888a5fe3523..37a594e4d2d374e497879fc32186de3af06251e0 100644 (file)
@@ -27,7 +27,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
 import org.openhab.core.library.types.StringType;
@@ -85,7 +84,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
         GenericBindingConfiguration config = getConfigAs(GenericBindingConfiguration.class);
         readCharacteristicJob = scheduler.scheduleWithFixedDelay(() -> {
             if (device.getConnectionState() == ConnectionState.CONNECTED) {
-                if (resolved) {
+                if (device.isServicesDiscovered()) {
                     handlerToChannels.forEach((charHandler, channelUids) -> {
                         // Only read the value manually if notification is not on.
                         // Also read it the first time before we activate notifications below.
@@ -139,11 +138,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
 
     @Override
     public void onServicesDiscovered() {
-        if (!resolved) {
-            resolved = true;
-            logger.trace("Service discovery completed for '{}'", address);
-            updateThingChannels();
-        }
+        super.onServicesDiscovered();
+        logger.trace("Service discovery completed for '{}'", address);
+        updateThingChannels();
     }
 
     @Override
@@ -157,19 +154,9 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
     }
 
     @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        super.onCharacteristicReadComplete(characteristic, status);
-        if (status == BluetoothCompletionStatus.SUCCESS) {
-            byte[] data = characteristic.getByteValue();
-            getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
-        }
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-        super.onCharacteristicUpdate(characteristic);
-        byte[] data = characteristic.getByteValue();
-        getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
+        super.onCharacteristicUpdate(characteristic, value);
+        getCharacteristicHandler(characteristic).handleCharacteristicUpdate(value);
     }
 
     private void updateThingChannels() {
@@ -207,13 +194,28 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
         return Objects.requireNonNull(charHandlers.computeIfAbsent(characteristic, CharacteristicHandler::new));
     }
 
-    private boolean readCharacteristic(BluetoothCharacteristic characteristic) {
-        return device.readCharacteristic(characteristic);
+    private void readCharacteristic(BluetoothCharacteristic characteristic) {
+        readCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid()).whenComplete((data, th) -> {
+            if (th != null) {
+                logger.warn("Could not read data from characteristic {} of device {}: {}", characteristic.getUuid(),
+                        address, th.getMessage());
+                return;
+            }
+            if (data != null) {
+                getCharacteristicHandler(characteristic).handleCharacteristicUpdate(data);
+            }
+        });
     }
 
-    private boolean writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
-        characteristic.setValue(data);
-        return device.writeCharacteristic(characteristic);
+    private void writeCharacteristic(BluetoothCharacteristic characteristic, byte[] data) {
+        writeCharacteristic(characteristic.getService().getUuid(), characteristic.getUuid(), data, false)
+                .whenComplete((r, th) -> {
+                    if (th != null) {
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                                "Could not write data to characteristic " + characteristic.getUuid() + ": "
+                                        + th.getMessage());
+                    }
+                });
     }
 
     private class CharacteristicHandler {
@@ -253,10 +255,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
                     } else if (state instanceof StringType) {
                         // unknown characteristic
                         byte[] data = HexUtils.hexToBytes(state.toString());
-                        if (!writeCharacteristic(characteristic, data)) {
-                            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                                    "Could not write data to characteristic: " + characteristicUUID);
-                        }
+                        writeCharacteristic(characteristic, data);
                     }
                 } catch (RuntimeException ex) {
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@@ -275,10 +274,7 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
                     BluetoothChannelUtils.updateHolder(gattParser, request, fieldName, state);
                     byte[] data = gattParser.serialize(request);
 
-                    if (!writeCharacteristic(characteristic, data)) {
-                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                                "Could not write data to characteristic: " + characteristicUUID);
-                    }
+                    writeCharacteristic(characteristic, data);
                 } catch (NumberFormatException ex) {
                     logger.warn("Could not parse characteristic value: {} : {}", characteristicUUID, state, ex);
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
index eef31338d1f222f0ebe36a28b2913c73894f042c..a4f76990c780cc3cc761d483f80e7103436670ce 100644 (file)
                                <description>The frequency at which readable characteristics refreshed</description>
                                <default>30</default>
                        </parameter>
+                       <parameter name="alwaysConnected" type="boolean">
+                               <label>Connect Automatically</label>
+                               <description>If enabled, will automatically connect to the device and reconnect if connection is lost.</description>
+                               <default>true</default>
+                       </parameter>
                </config-description>
 
        </thing-type>
diff --git a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java
deleted file mode 100644 (file)
index a6f3bdb..0000000
+++ /dev/null
@@ -1,472 +0,0 @@
-/**
- * Copyright (c) 2010-2021 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.bluetooth.govee.internal;
-
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.locks.Condition;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
-import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
-import org.openhab.binding.bluetooth.BluetoothDescriptor;
-import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
-import org.openhab.binding.bluetooth.BluetoothService;
-import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
-import org.openhab.core.common.NamedThreadFactory;
-import org.openhab.core.thing.Thing;
-import org.openhab.core.thing.ThingStatus;
-import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.util.HexUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
- *
- * @author Kai Kreuzer - Initial contribution and API
- * @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
- */
-@Deprecated
-@NonNullByDefault
-public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
-
-    private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
-
-    private final Condition connectionCondition = deviceLock.newCondition();
-    private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
-    private final Condition charCompleteCondition = deviceLock.newCondition();
-
-    private @Nullable Future<?> reconnectJob;
-    private @Nullable Future<?> pendingDisconnect;
-    private @Nullable BluetoothCharacteristic ongoingCharacteristic;
-    private @Nullable BluetoothCompletionStatus completeStatus;
-
-    private boolean connectOnDemand;
-    private int idleDisconnectDelayMs = 1000;
-
-    protected @Nullable ScheduledExecutorService connectionTaskExecutor;
-    private volatile boolean servicesDiscovered;
-
-    public ConnectedBluetoothHandler(Thing thing) {
-        super(thing);
-    }
-
-    @Override
-    public void initialize() {
-
-        // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
-        // the connectionTaskExecutor first
-        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
-                new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true));
-        executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
-        executor.setRemoveOnCancelPolicy(true);
-        connectionTaskExecutor = executor;
-
-        super.initialize();
-
-        connectOnDemand = true;
-
-        Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
-        idleDisconnectDelayMs = 1000;
-        if (idleDisconnectDelayRaw instanceof Number) {
-            idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
-        }
-
-        if (!connectOnDemand) {
-            reconnectJob = executor.scheduleWithFixedDelay(() -> {
-                try {
-                    if (device.getConnectionState() != ConnectionState.CONNECTED) {
-                        device.connect();
-                        // we do not set the Thing status here, because we will anyhow receive a call to
-                        // onConnectionStateChange
-                    } else {
-                        // just in case it was already connected to begin with
-                        updateStatus(ThingStatus.ONLINE);
-                        if (!servicesDiscovered && !device.discoverServices()) {
-                            logger.debug("Error while discovering services");
-                        }
-                    }
-                } catch (RuntimeException ex) {
-                    logger.warn("Unexpected error occurred", ex);
-                }
-            }, 0, 30, TimeUnit.SECONDS);
-        }
-    }
-
-    @Override
-    public void dispose() {
-        cancel(reconnectJob);
-        reconnectJob = null;
-        cancel(pendingDisconnect);
-        pendingDisconnect = null;
-
-        super.dispose();
-
-        shutdown(connectionTaskExecutor);
-        connectionTaskExecutor = null;
-    }
-
-    private static void cancel(@Nullable Future<?> future) {
-        if (future != null) {
-            future.cancel(true);
-        }
-    }
-
-    private void shutdown(@Nullable ScheduledExecutorService executor) {
-        if (executor != null) {
-            executor.shutdownNow();
-        }
-    }
-
-    private ScheduledExecutorService getConnectionTaskExecutor() {
-        var executor = connectionTaskExecutor;
-        if (executor == null) {
-            throw new IllegalStateException("characteristicScheduler has not been initialized");
-        }
-        return executor;
-    }
-
-    private void scheduleDisconnect() {
-        cancel(pendingDisconnect);
-        pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
-                TimeUnit.MILLISECONDS);
-    }
-
-    private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
-        if (device.getConnectionState() == ConnectionState.CONNECTED) {
-            return;
-        }
-        if (device.getConnectionState() != ConnectionState.CONNECTING) {
-            if (!device.connect()) {
-                throw new ConnectionException("Failed to start connecting");
-            }
-        }
-        logger.debug("waiting for connection");
-        if (!awaitConnection(1, TimeUnit.SECONDS)) {
-            throw new TimeoutException("Connection attempt timeout.");
-        }
-        logger.debug("connection successful");
-        if (!servicesDiscovered) {
-            logger.debug("discovering services");
-            device.discoverServices();
-            if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
-                throw new TimeoutException("Service discovery timeout");
-            }
-            logger.debug("service discovery successful");
-        }
-    }
-
-    private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
-        deviceLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (device.getConnectionState() != ConnectionState.CONNECTED) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            deviceLock.unlock();
-        }
-        return true;
-    }
-
-    private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
-        deviceLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (ongoingCharacteristic != null) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            deviceLock.unlock();
-        }
-        return true;
-    }
-
-    private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
-        deviceLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (!servicesDiscovered) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            deviceLock.unlock();
-        }
-        return true;
-    }
-
-    private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
-            throws BluetoothException, TimeoutException, InterruptedException {
-        connectAndWait();
-        BluetoothService service = device.getServices(serviceUUID);
-        if (service == null) {
-            throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
-        }
-        BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
-        if (characteristic == null) {
-            throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
-        }
-        return characteristic;
-    }
-
-    private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
-            CallableFunction<BluetoothCharacteristic, T> callable) {
-        CompletableFuture<T> future = new CompletableFuture<>();
-        var executor = connectionTaskExecutor;
-        if (executor != null) {
-            executor.execute(() -> {
-                cancel(pendingDisconnect);
-                try {
-                    BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
-                            characteristicUUID);
-                    future.complete(callable.call(characteristic));
-                } catch (InterruptedException e) {
-                    future.completeExceptionally(e);
-                    return;// we don't want to schedule anything if we receive an interrupt
-                } catch (TimeoutException e) {
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
-                    future.completeExceptionally(e);
-                } catch (Exception e) {
-                    future.completeExceptionally(e);
-                }
-                if (connectOnDemand) {
-                    scheduleDisconnect();
-                }
-            });
-        } else {
-            future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
-        }
-        return future;
-    }
-
-    public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
-        return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
-            if (!device.enableNotifications(characteristic)) {
-                throw new BluetoothException(
-                        "Failed to start notifications for characteristic: " + characteristic.getUuid());
-            }
-            return null;
-        });
-    }
-
-    public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
-            boolean enableNotification) {
-        return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
-            if (enableNotification) {
-                if (!device.enableNotifications(characteristic)) {
-                    throw new BluetoothException(
-                            "Failed to start characteristic notification" + characteristic.getUuid());
-                }
-            }
-            // now block for completion
-            characteristic.setValue(data);
-            ongoingCharacteristic = characteristic;
-            if (!device.writeCharacteristic(characteristic)) {
-                throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
-            }
-            if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
-                ongoingCharacteristic = null;
-                throw new TimeoutException(
-                        "Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
-            }
-            if (completeStatus == BluetoothCompletionStatus.ERROR) {
-                throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
-            }
-            logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
-                    characteristic.getUuid(), address);
-            return null;
-        });
-    }
-
-    public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
-        return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
-            // now block for completion
-            ongoingCharacteristic = characteristic;
-            if (!device.readCharacteristic(characteristic)) {
-                throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
-            }
-            if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
-                ongoingCharacteristic = null;
-                throw new TimeoutException(
-                        "Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
-            }
-            if (completeStatus == BluetoothCompletionStatus.ERROR) {
-                throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
-            }
-            byte[] data = characteristic.getByteValue();
-            logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
-                    HexUtils.bytesToHex(data));
-            return data;
-        });
-    }
-
-    @Override
-    protected void updateStatusBasedOnRssi(boolean receivedSignal) {
-        // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
-        // we are connected.
-        if (receivedSignal) {
-            if (device.getConnectionState() == ConnectionState.CONNECTED) {
-                updateStatus(ThingStatus.ONLINE);
-            } else {
-                if (!connectOnDemand) {
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
-                }
-            }
-        } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
-        }
-    }
-
-    @Override
-    public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
-        super.onConnectionStateChange(connectionNotification);
-        switch (connectionNotification.getConnectionState()) {
-            case DISCOVERED:
-                // The device is now known on the Bluetooth network, so we can do something...
-                if (!connectOnDemand) {
-                    getConnectionTaskExecutor().submit(() -> {
-                        if (device.getConnectionState() != ConnectionState.CONNECTED) {
-                            if (!device.connect()) {
-                                logger.debug("Error connecting to device after discovery.");
-                            }
-                        }
-                    });
-                }
-                break;
-            case CONNECTED:
-                deviceLock.lock();
-                try {
-                    connectionCondition.signal();
-                } finally {
-                    deviceLock.unlock();
-                }
-                if (!connectOnDemand) {
-                    getConnectionTaskExecutor().submit(() -> {
-                        if (!servicesDiscovered && !device.discoverServices()) {
-                            logger.debug("Error while discovering services");
-                        }
-                    });
-                }
-                break;
-            case DISCONNECTED:
-                var future = pendingDisconnect;
-                if (future != null) {
-                    future.cancel(false);
-                }
-                if (!connectOnDemand) {
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
-                }
-                break;
-            default:
-                break;
-        }
-    }
-
-    @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        super.onCharacteristicReadComplete(characteristic, status);
-        deviceLock.lock();
-        try {
-            if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
-                completeStatus = status;
-                ongoingCharacteristic = null;
-                charCompleteCondition.signal();
-            }
-        } finally {
-            deviceLock.unlock();
-        }
-    }
-
-    @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-        super.onCharacteristicWriteComplete(characteristic, status);
-        deviceLock.lock();
-        try {
-            if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
-                completeStatus = status;
-                ongoingCharacteristic = null;
-                charCompleteCondition.signal();
-            }
-        } finally {
-            deviceLock.unlock();
-        }
-    }
-
-    @Override
-    public void onServicesDiscovered() {
-        super.onServicesDiscovered();
-        deviceLock.lock();
-        try {
-            this.servicesDiscovered = true;
-            serviceDiscoveryCondition.signal();
-        } finally {
-            deviceLock.unlock();
-        }
-        logger.debug("Service discovery completed for '{}'", address);
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-        super.onCharacteristicUpdate(characteristic);
-        if (logger.isDebugEnabled()) {
-            logger.debug("Recieved update {} to characteristic {} of device {}",
-                    HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
-        }
-    }
-
-    @Override
-    public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
-        super.onDescriptorUpdate(descriptor);
-        if (logger.isDebugEnabled()) {
-            logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
-                    descriptor.getUuid(), address);
-        }
-    }
-
-    public static class BluetoothException extends Exception {
-
-        public BluetoothException(String message) {
-            super(message);
-        }
-    }
-
-    public static class ConnectionException extends BluetoothException {
-
-        public ConnectionException(String message) {
-            super(message);
-        }
-    }
-
-    @FunctionalInterface
-    public static interface CallableFunction<U, R> {
-        public R call(U arg) throws Exception;
-    }
-}
index 21caa88cfff3fbdff0f86c38a6b11c877e116637..6072c3a6dbaa5430d81291b9928af4380ace5791 100644 (file)
@@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
+import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
 import org.openhab.binding.bluetooth.gattserial.MessageServicer;
 import org.openhab.binding.bluetooth.gattserial.SimpleGattSocket;
 import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetBatteryCommand;
@@ -93,6 +94,11 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
     @Override
     public void initialize() {
         super.initialize();
+        if (thing.getStatus() == ThingStatus.OFFLINE) {
+            // something went wrong in super.initialize() so we shouldn't initialize further here either
+            return;
+        }
+
         config = getConfigAs(GoveeHygrometerConfiguration.class);
 
         Map<String, String> properties = thing.getProperties();
@@ -117,14 +123,14 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
                     logger.debug("refreshing temperature, humidity, and battery");
                     refreshBattery().join();
                     refreshTemperatureAndHumidity().join();
-                    connectionTaskExecutor.execute(device::disconnect);
+                    disconnect();
                     updateStatus(ThingStatus.ONLINE);
                 }
             } catch (RuntimeException ex) {
                 logger.warn("unable to refresh", ex);
             }
         }, 0, config.refreshInterval, TimeUnit.SECONDS);
-        keepAliveJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
+        keepAliveJob = scheduler.scheduleWithFixedDelay(() -> {
             if (device.getConnectionState() == ConnectionState.CONNECTED) {
                 try {
                     GoveeMessage message = new GoveeMessage((byte) 0xAA, (byte) 1, null);
@@ -393,9 +399,9 @@ public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
     }
 
     @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-        super.onCharacteristicUpdate(characteristic);
-        commandSocket.receivePacket(characteristic.getByteValue());
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
+        super.onCharacteristicUpdate(characteristic, value);
+        commandSocket.receivePacket(value);
     }
 
     private class CommandSocket extends SimpleGattSocket<GoveeMessage> {
index 92f3af7b7a46fac79f27803f1ab6fe04919a5a36..79cbcfd937c87dd1071ad10c52dc672b7c03dfdb 100644 (file)
@@ -25,7 +25,6 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothAdapter;
 import org.openhab.binding.bluetooth.BluetoothAddress;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
 import org.openhab.binding.bluetooth.BluetoothDescriptor;
 import org.openhab.binding.bluetooth.BluetoothDevice;
 import org.openhab.binding.bluetooth.BluetoothDeviceListener;
@@ -135,32 +134,16 @@ public class RoamingBluetoothDevice extends DelegateBluetoothDevice {
         }
 
         @Override
-        public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic,
-                BluetoothCompletionStatus status) {
+        public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
             if (device == getDelegate()) {
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic, status);
+                notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic, value);
             }
         }
 
         @Override
-        public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-                BluetoothCompletionStatus status) {
+        public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
             if (device == getDelegate()) {
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic);
-            }
-        }
-
-        @Override
-        public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-            if (device == getDelegate()) {
-                notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
-            }
-        }
-
-        @Override
-        public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
-            if (device == getDelegate()) {
-                notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor);
+                notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, bluetoothDescriptor, value);
             }
         }
 
index 210d43213001d2c0cb1c4e6a9132db8c702b96b6..c6422cc7462896a2b32c821ffa961b9b5b00d833 100644 (file)
@@ -19,6 +19,10 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -80,6 +84,12 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
      */
     private final Set<BluetoothDeviceListener> eventListeners = new CopyOnWriteArraySet<>();
 
+    private final Lock deviceLock = new ReentrantLock();
+    private final Condition connectionCondition = deviceLock.newCondition();
+    private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
+
+    private volatile boolean servicesDiscovered = false;
+
     /**
      * Construct a Bluetooth device taking the Bluetooth address
      *
@@ -258,6 +268,45 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
     protected void dispose() {
     }
 
+    @Override
+    public boolean isServicesDiscovered() {
+        return servicesDiscovered;
+    }
+
+    @Override
+    public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
+        deviceLock.lock();
+        try {
+            long nanosTimeout = unit.toNanos(timeout);
+            while (getConnectionState() != ConnectionState.CONNECTED) {
+                if (nanosTimeout <= 0L) {
+                    return false;
+                }
+                nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
+            }
+        } finally {
+            deviceLock.unlock();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
+        deviceLock.lock();
+        try {
+            long nanosTimeout = unit.toNanos(timeout);
+            while (!servicesDiscovered) {
+                if (nanosTimeout <= 0L) {
+                    return false;
+                }
+                nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
+            }
+        } finally {
+            deviceLock.unlock();
+        }
+        return true;
+    }
+
     @Override
     protected void notifyListeners(BluetoothEventType event, Object... args) {
         switch (event) {
@@ -270,6 +319,27 @@ public abstract class BaseBluetoothDevice extends BluetoothDevice {
             default:
                 break;
         }
+        switch (event) {
+            case SERVICES_DISCOVERED:
+                deviceLock.lock();
+                try {
+                    servicesDiscovered = true;
+                    serviceDiscoveryCondition.signal();
+                } finally {
+                    deviceLock.unlock();
+                }
+                break;
+            case CONNECTION_STATE:
+                deviceLock.lock();
+                try {
+                    connectionCondition.signal();
+                } finally {
+                    deviceLock.unlock();
+                }
+                break;
+            default:
+                break;
+        }
         super.notifyListeners(event, args);
     }
 
index c1602e52d216ab2a80dcb0892462b1245e74a884..ac39941c87cb4ebac5e09a214418d42de876af53 100644 (file)
@@ -214,7 +214,7 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
         }
     }
 
-    private void onActivity() {
+    protected void onActivity() {
         this.lastActivityTime = ZonedDateTime.now();
     }
 
@@ -241,27 +241,12 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
     }
 
     @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        if (status == BluetoothCompletionStatus.SUCCESS) {
-            onActivity();
-        }
-    }
-
-    @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-        if (status == BluetoothCompletionStatus.SUCCESS) {
-            onActivity();
-        }
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
         onActivity();
     }
 
     @Override
-    public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
+    public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
         onActivity();
     }
 
index 9e8a591bcad02c753f770dec89a508833b3d3ae9..77509f1551166f69e95ed130912fb6ca8559426d 100644 (file)
@@ -43,6 +43,8 @@ public class BluetoothBindingConstants {
 
     public static final String CONFIGURATION_ADDRESS = "address";
     public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";
+    public static final String CONFIGURATION_ALWAYS_CONNECTED = "alwaysConnected";
+    public static final String CONFIGURATION_IDLE_DISCONNECT_DELAY = "idleDisconnectDelay";
 
     public static final long BLUETOOTH_BASE_UUID = 0x800000805f9b34fbL;
 
index dd591acc723f65058572c72fd283caac64d1a2c6..b452997df530867e32ac14c24e870f0e9c85a95d 100644 (file)
@@ -12,7 +12,6 @@
  */
 package org.openhab.binding.bluetooth;
 
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -34,15 +33,6 @@ import org.slf4j.LoggerFactory;
  * @author Peter Rosenberg - Improve properties support
  */
 public class BluetoothCharacteristic {
-    public static final int FORMAT_UINT8 = 0x11;
-    public static final int FORMAT_UINT16 = 0x12;
-    public static final int FORMAT_UINT32 = 0x14;
-    public static final int FORMAT_SINT8 = 0x21;
-    public static final int FORMAT_SINT16 = 0x22;
-    public static final int FORMAT_SINT32 = 0x24;
-    public static final int FORMAT_SFLOAT = 0x32;
-    public static final int FORMAT_FLOAT = 0x34;
-
     public static final int PROPERTY_BROADCAST = 0x01;
     public static final int PROPERTY_READ = 0x02;
     public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
@@ -86,11 +76,6 @@ public class BluetoothCharacteristic {
     protected int permissions;
     protected int writeType;
 
-    /**
-     * The raw data value for this characteristic
-     */
-    protected int[] value = new int[0];
-
     /**
      * The {@link BluetoothService} to which this characteristic belongs
      */
@@ -314,299 +299,6 @@ public class BluetoothCharacteristic {
         return true;
     }
 
-    /**
-     * Get the stored value for this characteristic.
-     *
-     */
-    public int[] getValue() {
-        return value;
-    }
-
-    /**
-     * Get the stored value for this characteristic.
-     *
-     */
-    public byte[] getByteValue() {
-        byte[] byteValue = new byte[value.length];
-        for (int cnt = 0; cnt < value.length; cnt++) {
-            byteValue[cnt] = (byte) (value[cnt] & 0xFF);
-        }
-        return byteValue;
-    }
-
-    /**
-     * Return the stored value of this characteristic.
-     *
-     */
-    public Integer getIntValue(int formatType, int offset) {
-        if ((offset + getTypeLen(formatType)) > value.length) {
-            return null;
-        }
-
-        switch (formatType) {
-            case FORMAT_UINT8:
-                return unsignedByteToInt(value[offset]);
-
-            case FORMAT_UINT16:
-                return unsignedBytesToInt(value[offset], value[offset + 1]);
-
-            case FORMAT_UINT32:
-                return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
-
-            case FORMAT_SINT8:
-                return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
-
-            case FORMAT_SINT16:
-                return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
-
-            case FORMAT_SINT32:
-                return unsignedToSigned(
-                        unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
-            default:
-                logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
-        }
-
-        return null;
-    }
-
-    /**
-     * Return the stored value of this characteristic. This doesn't read the remote data.
-     *
-     */
-    public Float getFloatValue(int formatType, int offset) {
-        if ((offset + getTypeLen(formatType)) > value.length) {
-            return null;
-        }
-
-        switch (formatType) {
-            case FORMAT_SFLOAT:
-                return bytesToFloat(value[offset], value[offset + 1]);
-            case FORMAT_FLOAT:
-                return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
-            default:
-                logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
-        }
-
-        return null;
-    }
-
-    /**
-     * Return the stored value of this characteristic. This doesn't read the remote data.
-     *
-     */
-    public String getStringValue(int offset) {
-        if (value == null || offset > value.length) {
-            return null;
-        }
-        byte[] strBytes = new byte[value.length - offset];
-        for (int i = 0; i < (value.length - offset); ++i) {
-            strBytes[i] = (byte) value[offset + i];
-        }
-        return new String(strBytes, StandardCharsets.UTF_8);
-    }
-
-    /**
-     * Updates the locally stored value of this characteristic.
-     *
-     * @param value the value to set
-     * @return true, if it has been set successfully
-     */
-    public boolean setValue(int[] value) {
-        this.value = value;
-        return true;
-    }
-
-    /**
-     * Set the local value of this characteristic.
-     *
-     * @param value the value to set
-     * @param formatType the format of the value (as one of the FORMAT_* constants in this class)
-     * @param offset the offset to use when interpreting the value
-     * @return true, if it has been set successfully
-     */
-    public boolean setValue(int value, int formatType, int offset) {
-        int len = offset + getTypeLen(formatType);
-        if (this.value == null) {
-            this.value = new int[len];
-        }
-        if (len > this.value.length) {
-            return false;
-        }
-        int val = value;
-        switch (formatType) {
-            case FORMAT_SINT8:
-                val = intToSignedBits(value, 8);
-                // Fall-through intended
-            case FORMAT_UINT8:
-                this.value[offset] = (byte) (val & 0xFF);
-                break;
-
-            case FORMAT_SINT16:
-                val = intToSignedBits(value, 16);
-                // Fall-through intended
-            case FORMAT_UINT16:
-                this.value[offset] = (byte) (val & 0xFF);
-                this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
-                break;
-
-            case FORMAT_SINT32:
-                val = intToSignedBits(value, 32);
-                // Fall-through intended
-            case FORMAT_UINT32:
-                this.value[offset] = (byte) (val & 0xFF);
-                this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
-                this.value[offset + 2] = (byte) ((val >> 16) & 0xFF);
-                this.value[offset + 2] = (byte) ((val >> 24) & 0xFF);
-                break;
-
-            default:
-                return false;
-        }
-        return true;
-    }
-
-    /**
-     * Set the local value of this characteristic.
-     *
-     * @param mantissa the mantissa of the value
-     * @param exponent the exponent of the value
-     * @param formatType the format of the value (as one of the FORMAT_* constants in this class)
-     * @param offset the offset to use when interpreting the value
-     * @return true, if it has been set successfully
-     *
-     */
-    public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
-        int len = offset + getTypeLen(formatType);
-        if (value == null) {
-            value = new int[len];
-        }
-        if (len > value.length) {
-            return false;
-        }
-
-        switch (formatType) {
-            case FORMAT_SFLOAT:
-                int m = intToSignedBits(mantissa, 12);
-                int exp = intToSignedBits(exponent, 4);
-                value[offset] = (byte) (m & 0xFF);
-                value[offset + 1] = (byte) ((m >> 8) & 0x0F);
-                value[offset + 1] += (byte) ((exp & 0x0F) << 4);
-                break;
-
-            case FORMAT_FLOAT:
-                m = intToSignedBits(mantissa, 24);
-                exp = intToSignedBits(exponent, 8);
-                value[offset] = (byte) (m & 0xFF);
-                value[offset + 1] = (byte) ((m >> 8) & 0xFF);
-                value[offset + 2] = (byte) ((m >> 16) & 0xFF);
-                value[offset + 2] += (byte) (exp & 0xFF);
-                break;
-
-            default:
-                return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Set the local value of this characteristic.
-     *
-     * @param value the value to set
-     * @return true, if it has been set successfully
-     */
-    public boolean setValue(byte[] value) {
-        this.value = new int[value.length];
-        int cnt = 0;
-        for (byte val : value) {
-            this.value[cnt++] = val;
-        }
-        return true;
-    }
-
-    /**
-     * Set the local value of this characteristic.
-     *
-     * @param value the value to set
-     * @return true, if it has been set successfully
-     */
-    public boolean setValue(String value) {
-        this.value = new int[value.getBytes().length];
-        int cnt = 0;
-        for (byte val : value.getBytes()) {
-            this.value[cnt++] = val;
-        }
-        return true;
-    }
-
-    /**
-     * Returns the size of the requested value type.
-     */
-    private int getTypeLen(int formatType) {
-        return formatType & 0xF;
-    }
-
-    /**
-     * Convert a signed byte to an unsigned int.
-     */
-    private int unsignedByteToInt(int value) {
-        return value & 0xFF;
-    }
-
-    /**
-     * Convert signed bytes to a 16-bit unsigned int.
-     */
-    private int unsignedBytesToInt(int value1, int value2) {
-        return value1 + (value2 << 8);
-    }
-
-    /**
-     * Convert signed bytes to a 32-bit unsigned int.
-     */
-    private int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
-        return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
-    }
-
-    /**
-     * Convert signed bytes to a 16-bit short float value.
-     */
-    private float bytesToFloat(int value1, int value2) {
-        int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
-        int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
-        return (float) (mantissa * Math.pow(10, exponent));
-    }
-
-    /**
-     * Convert signed bytes to a 32-bit short float value.
-     */
-    private float bytesToFloat(int value1, int value2, int value3, int value4) {
-        int mantissa = unsignedToSigned(
-                unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
-        return (float) (mantissa * Math.pow(10, value4));
-    }
-
-    /**
-     * Convert an unsigned integer to a two's-complement signed value.
-     */
-    private int unsignedToSigned(int unsigned, int size) {
-        if ((unsigned & (1 << size - 1)) != 0) {
-            return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
-        } else {
-            return unsigned;
-        }
-    }
-
-    /**
-     * Convert an integer into the signed bits of the specified length.
-     */
-    private int intToSignedBits(int i, int size) {
-        if (i < 0) {
-            return (1 << size - 1) + (i & ((1 << size - 1) - 1));
-        } else {
-            return i;
-        }
-    }
-
     public GattCharacteristic getGattCharacteristic() {
         return GattCharacteristic.getCharacteristic(uuid);
     }
index ec6c131caad45361356a1d7ea8dce96197ecb775..c3afcc08bc21050084f09bd78ecbda47b3b9548d 100644 (file)
@@ -30,8 +30,8 @@ public class BluetoothDescriptor {
 
     protected final BluetoothCharacteristic characteristic;
     protected final UUID uuid;
+
     protected final int handle;
-    protected byte[] value;
 
     /**
      * The main constructor
@@ -81,24 +81,6 @@ public class BluetoothDescriptor {
         return handle;
     }
 
-    /**
-     * Returns the stored value for this descriptor. It doesn't read remote data.
-     *
-     * @return the value of the descriptor
-     */
-    public byte[] getValue() {
-        return value;
-    }
-
-    /**
-     * Sets the stored value for this descriptor. It doesn't update remote data.
-     *
-     * @param value the value for this descriptor instance
-     */
-    public void setValue(byte[] value) {
-        this.value = value;
-    }
-
     public GattDescriptor getDescriptor() {
         return GattDescriptor.getDescriptor(uuid);
     }
index ba1186829f12bf3749b57367ee2861db78bdc7d5..a1616f9ae344a5f88ee441f348d984fcc4e2b1a8 100644 (file)
@@ -14,6 +14,8 @@ package org.openhab.binding.bluetooth;
 
 import java.util.Collection;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -69,8 +71,6 @@ public abstract class BluetoothDevice {
     protected enum BluetoothEventType {
         CONNECTION_STATE,
         SCAN_RECORD,
-        CHARACTERISTIC_READ_COMPLETE,
-        CHARACTERISTIC_WRITE_COMPLETE,
         CHARACTERISTIC_UPDATED,
         DESCRIPTOR_UPDATED,
         SERVICES_DISCOVERED,
@@ -227,28 +227,25 @@ public abstract class BluetoothDevice {
      * Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
      * operation when one is already in progress will result in subsequent calls returning false.
      * <p>
-     * This is an asynchronous method. Once the read is complete
-     * {@link BluetoothDeviceListener.onCharacteristicReadComplete}
-     * method will be called with the completion state.
-     * <p>
-     * Note that {@link BluetoothDeviceListener.onCharacteristicUpdate} will be called when the read value is received.
+     * This is an asynchronous method. Once the read is complete the returned future will be updated with the result.
      *
      * @param characteristic the {@link BluetoothCharacteristic} to read.
-     * @return true if the characteristic read is started successfully
+     * @return a future that returns the read data is successful, otherwise throws an exception
      */
-    public abstract boolean readCharacteristic(BluetoothCharacteristic characteristic);
+    public abstract CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic);
 
     /**
      * Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
      * operation when one is already in progress will result in subsequent calls returning false.
      * <p>
-     * This is an asynchronous method. Once the write is complete
-     * {@link BluetoothDeviceListener.onCharacteristicWriteComplete} method will be called with the completion state.
+     * This is an asynchronous method. Once the write is complete the returned future will be updated with the result.
      *
-     * @param characteristic the {@link BluetoothCharacteristic} to read.
-     * @return true if the characteristic write is started successfully
+     * @param characteristic the {@link BluetoothCharacteristic} to write.
+     * @param value the data to write
+     * @return a future that returns null upon a successful write, otherwise throws an exception
      */
-    public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic);
+    public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic,
+            byte[] value);
 
     /**
      * Returns if notification is enabled for the given characteristic.
@@ -269,7 +266,7 @@ public abstract class BluetoothDevice {
      * @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
      * @return true if the characteristic notification is started successfully
      */
-    public abstract boolean enableNotifications(BluetoothCharacteristic characteristic);
+    public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic);
 
     /**
      * Disables notifications for a characteristic. Only a single read or write operation can be requested at once.
@@ -279,7 +276,7 @@ public abstract class BluetoothDevice {
      * @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
      * @return true if the characteristic notification is stopped successfully
      */
-    public abstract boolean disableNotifications(BluetoothCharacteristic characteristic);
+    public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic);
 
     /**
      * Enables notifications for a descriptor. Only a single read or write operation can be requested at once.
@@ -376,6 +373,12 @@ public abstract class BluetoothDevice {
 
     protected abstract Collection<BluetoothDeviceListener> getListeners();
 
+    public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException;
+
+    public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException;
+
+    public abstract boolean isServicesDiscovered();
+
     /**
      * Notify the listeners of an event
      *
@@ -395,19 +398,11 @@ public abstract class BluetoothDevice {
                     case SERVICES_DISCOVERED:
                         listener.onServicesDiscovered();
                         break;
-                    case CHARACTERISTIC_READ_COMPLETE:
-                        listener.onCharacteristicReadComplete((BluetoothCharacteristic) args[0],
-                                (BluetoothCompletionStatus) args[1]);
-                        break;
-                    case CHARACTERISTIC_WRITE_COMPLETE:
-                        listener.onCharacteristicWriteComplete((BluetoothCharacteristic) args[0],
-                                (BluetoothCompletionStatus) args[1]);
-                        break;
                     case CHARACTERISTIC_UPDATED:
-                        listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0]);
+                        listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]);
                         break;
                     case DESCRIPTOR_UPDATED:
-                        listener.onDescriptorUpdate((BluetoothDescriptor) args[0]);
+                        listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]);
                         break;
                     case ADAPTER_CHANGED:
                         listener.onAdapterChanged((BluetoothAdapter) args[0]);
index 858ac0ef2c77ce4d88dc08836bed497cd0732d85..4220cd8e1358a03a934358fa6c7474f9dc1093bd 100644 (file)
@@ -46,37 +46,23 @@ public interface BluetoothDeviceListener {
      */
     void onServicesDiscovered();
 
-    /**
-     * Called when a read request completes
-     *
-     * @param characteristic the {@link BluetoothCharacteristic} that has completed the read request
-     * @param status the {@link BluetoothCompletionStatus} of the read request
-     */
-    void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
-
-    /**
-     * Called when a write request completes
-     *
-     * @param characteristic the {@link BluetoothCharacteristic} that has completed the write request
-     * @param status the {@link BluetoothCompletionStatus} of the write request
-     */
-    void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
-
     /**
      * Called when a characteristic value is received. Implementations should call this whenever a value
      * is received from the BLE device even if there is no change to the value.
      *
      * @param characteristic the updated {@link BluetoothCharacteristic}
+     * @param value the update value
      */
-    void onCharacteristicUpdate(BluetoothCharacteristic characteristic);
+    void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value);
 
     /**
      * Called when a descriptor value is received. Implementations should call this whenever a value
      * is received from the BLE device even if there is no change to the value.
      *
-     * @param characteristic the updated {@link BluetoothCharacteristic}
+     * @param bluetoothDescriptor the updated {@link BluetoothDescriptor}
+     * @param value the update value
      */
-    void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor);
+    void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value);
 
     /**
      * Called when the BluetoothAdapter for this BluetoothDevice changes.
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java
new file mode 100644 (file)
index 0000000..d4977a1
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bluetooth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This class encompasses exceptions that occur within the bluetooth api. This can be subclassed for more specific
+ * exceptions in api implementations.
+ *
+ * @author Connor Petty - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class BluetoothException extends Exception {
+
+    private static final long serialVersionUID = -2557298438595050148L;
+
+    public BluetoothException() {
+        super();
+    }
+
+    public BluetoothException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+    public BluetoothException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public BluetoothException(String message) {
+        super(message);
+    }
+
+    public BluetoothException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java
new file mode 100644 (file)
index 0000000..454a56b
--- /dev/null
@@ -0,0 +1,292 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bluetooth;
+
+import java.nio.charset.StandardCharsets;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a utility class for parsing or formatting bluetooth characteristic data.
+ *
+ * @author Connor Petty - Initial Contribution
+ *
+ */
+public class BluetoothUtils {
+
+    public static final Logger logger = LoggerFactory.getLogger(BluetoothUtils.class);
+
+    public static final int FORMAT_UINT8 = 0x11;
+    public static final int FORMAT_UINT16 = 0x12;
+    public static final int FORMAT_UINT32 = 0x14;
+    public static final int FORMAT_SINT8 = 0x21;
+    public static final int FORMAT_SINT16 = 0x22;
+    public static final int FORMAT_SINT32 = 0x24;
+    public static final int FORMAT_SFLOAT = 0x32;
+    public static final int FORMAT_FLOAT = 0x34;
+
+    /**
+     * Converts a byte array to an int array
+     *
+     * @param value
+     * @return
+     */
+    public static int[] toIntArray(byte[] value) {
+        if (value == null) {
+            return null;
+        }
+        int[] ret = new int[value.length];
+        for (int i = 0; i < value.length; i++) {
+            ret[i] = value[i];
+        }
+        return ret;
+    }
+
+    public static byte[] toByteArray(int[] value) {
+        if (value == null) {
+            return null;
+        }
+        byte[] ret = new byte[value.length];
+        for (int i = 0; i < value.length; i++) {
+            ret[i] = (byte) (value[i] & 0xFF);
+        }
+        return ret;
+    }
+
+    /**
+     * Return the stored value of this characteristic.
+     *
+     */
+    public static Integer getIntegerValue(byte[] value, int formatType, int offset) {
+        if ((offset + getTypeLen(formatType)) > value.length) {
+            return null;
+        }
+
+        switch (formatType) {
+            case FORMAT_UINT8:
+                return unsignedByteToInt(value[offset]);
+
+            case FORMAT_UINT16:
+                return unsignedBytesToInt(value[offset], value[offset + 1]);
+
+            case FORMAT_UINT32:
+                return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
+
+            case FORMAT_SINT8:
+                return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
+
+            case FORMAT_SINT16:
+                return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
+
+            case FORMAT_SINT32:
+                return unsignedToSigned(
+                        unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
+            default:
+                logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the stored value of this characteristic. This doesn't read the remote data.
+     *
+     */
+    public static Float getFloatValue(byte[] value, int formatType, int offset) {
+        if ((offset + getTypeLen(formatType)) > value.length) {
+            return null;
+        }
+
+        switch (formatType) {
+            case FORMAT_SFLOAT:
+                return bytesToFloat(value[offset], value[offset + 1]);
+            case FORMAT_FLOAT:
+                return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
+            default:
+                logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
+        }
+
+        return null;
+    }
+
+    /**
+     * Return the stored value of this characteristic. This doesn't read the remote data.
+     *
+     */
+    public static String getStringValue(byte[] value, int offset) {
+        if (value == null || offset > value.length) {
+            return null;
+        }
+        byte[] strBytes = new byte[value.length - offset];
+        for (int i = 0; i < (value.length - offset); ++i) {
+            strBytes[i] = value[offset + i];
+        }
+        return new String(strBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Set the local value of this characteristic.
+     *
+     * @param value the value to set
+     * @param formatType the format of the value (as one of the FORMAT_* constants in this class)
+     * @param offset the offset to use when interpreting the value
+     * @return true, if it has been set successfully
+     */
+    public static boolean setValue(byte[] dest, int value, int formatType, int offset) {
+        int len = offset + getTypeLen(formatType);
+        if (dest == null || len > dest.length) {
+            return false;
+        }
+        int val = value;
+        switch (formatType) {
+            case FORMAT_SINT8:
+                val = intToSignedBits(value, 8);
+                // Fall-through intended
+            case FORMAT_UINT8:
+                dest[offset] = (byte) (val & 0xFF);
+                break;
+
+            case FORMAT_SINT16:
+                val = intToSignedBits(value, 16);
+                // Fall-through intended
+            case FORMAT_UINT16:
+                dest[offset] = (byte) (val & 0xFF);
+                dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
+                break;
+
+            case FORMAT_SINT32:
+                val = intToSignedBits(value, 32);
+                // Fall-through intended
+            case FORMAT_UINT32:
+                dest[offset] = (byte) (val & 0xFF);
+                dest[offset + 1] = (byte) ((val >> 8) & 0xFF);
+                dest[offset + 2] = (byte) ((val >> 16) & 0xFF);
+                dest[offset + 2] = (byte) ((val >> 24) & 0xFF);
+                break;
+
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set the local value of this characteristic.
+     *
+     * @param mantissa the mantissa of the value
+     * @param exponent the exponent of the value
+     * @param formatType the format of the value (as one of the FORMAT_* constants in this class)
+     * @param offset the offset to use when interpreting the value
+     * @return true, if it has been set successfully
+     *
+     */
+    public static boolean setValue(byte[] dest, int mantissa, int exponent, int formatType, int offset) {
+        int len = offset + getTypeLen(formatType);
+        if (dest == null || len > dest.length) {
+            return false;
+        }
+
+        switch (formatType) {
+            case FORMAT_SFLOAT:
+                int m = intToSignedBits(mantissa, 12);
+                int exp = intToSignedBits(exponent, 4);
+                dest[offset] = (byte) (m & 0xFF);
+                dest[offset + 1] = (byte) ((m >> 8) & 0x0F);
+                dest[offset + 1] += (byte) ((exp & 0x0F) << 4);
+                break;
+
+            case FORMAT_FLOAT:
+                m = intToSignedBits(mantissa, 24);
+                exp = intToSignedBits(exponent, 8);
+                dest[offset] = (byte) (m & 0xFF);
+                dest[offset + 1] = (byte) ((m >> 8) & 0xFF);
+                dest[offset + 2] = (byte) ((m >> 16) & 0xFF);
+                dest[offset + 2] += (byte) (exp & 0xFF);
+                break;
+
+            default:
+                return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the size of the requested value type.
+     */
+    private static int getTypeLen(int formatType) {
+        return formatType & 0xF;
+    }
+
+    /**
+     * Convert a signed byte to an unsigned int.
+     */
+    private static int unsignedByteToInt(int value) {
+        return value & 0xFF;
+    }
+
+    /**
+     * Convert signed bytes to a 16-bit unsigned int.
+     */
+    private static int unsignedBytesToInt(int value1, int value2) {
+        return value1 + (value2 << 8);
+    }
+
+    /**
+     * Convert signed bytes to a 32-bit unsigned int.
+     */
+    private static int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
+        return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
+    }
+
+    /**
+     * Convert signed bytes to a 16-bit short float value.
+     */
+    private static float bytesToFloat(int value1, int value2) {
+        int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
+        int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
+        return (float) (mantissa * Math.pow(10, exponent));
+    }
+
+    /**
+     * Convert signed bytes to a 32-bit short float value.
+     */
+    private static float bytesToFloat(int value1, int value2, int value3, int value4) {
+        int mantissa = unsignedToSigned(
+                unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
+        return (float) (mantissa * Math.pow(10, value4));
+    }
+
+    /**
+     * Convert an unsigned integer to a two's-complement signed value.
+     */
+    private static int unsignedToSigned(int unsigned, int size) {
+        if ((unsigned & (1 << size - 1)) != 0) {
+            return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
+        } else {
+            return unsigned;
+        }
+    }
+
+    /**
+     * Convert an integer into the signed bits of the specified length.
+     */
+    private static int intToSignedBits(int i, int size) {
+        if (i < 0) {
+            return (1 << size - 1) + (i & ((1 << size - 1) - 1));
+        } else {
+            return i;
+        }
+    }
+}
index 12ea302d56cbd3d5fbacd609c0e82581e3a15e43..556b23ee2f11bee3401dff367a69530cc1b0602d 100644 (file)
  */
 package org.openhab.binding.bluetooth;
 
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.ScheduledFuture;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
 
-import org.eclipse.jdt.annotation.DefaultLocation;
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
-import org.openhab.core.thing.ChannelUID;
+import org.openhab.binding.bluetooth.util.RetryFuture;
+import org.openhab.core.common.NamedThreadFactory;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
-import org.openhab.core.types.Command;
-import org.openhab.core.types.RefreshType;
 import org.openhab.core.util.HexUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,17 +41,18 @@ import org.slf4j.LoggerFactory;
  *
  * @author Kai Kreuzer - Initial contribution and API
  */
-@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
-        DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
+@NonNullByDefault
 public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
 
     private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
-    private ScheduledFuture<?> connectionJob;
+    private @Nullable Future<?> reconnectJob;
+    private @Nullable Future<?> pendingDisconnect;
 
-    // internal flag for the service resolution status
-    protected volatile boolean resolved = false;
+    private boolean alwaysConnected;
+    private int idleDisconnectDelay = 1000;
 
-    protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
+    // we initially set the to scheduler so that we can keep this field non-null
+    private ScheduledExecutorService connectionTaskExecutor = scheduler;
 
     public ConnectedBluetoothHandler(Thing thing) {
         super(thing);
@@ -54,55 +60,198 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
 
     @Override
     public void initialize() {
+
+        // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
+        // the connectionTaskExecutor first
+        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
+                new NamedThreadFactory("bluetooth-connection" + thing.getThingTypeUID(), true));
+        executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+        executor.setRemoveOnCancelPolicy(true);
+        connectionTaskExecutor = executor;
+
         super.initialize();
 
-        connectionJob = scheduler.scheduleWithFixedDelay(() -> {
-            try {
-                if (device.getConnectionState() != ConnectionState.CONNECTED) {
-                    device.connect();
-                    // we do not set the Thing status here, because we will anyhow receive a call to
-                    // onConnectionStateChange
-                } else {
-                    // just in case it was already connected to begin with
-                    updateStatus(ThingStatus.ONLINE);
-                    if (!resolved && !device.discoverServices()) {
-                        logger.debug("Error while discovering services");
+        if (thing.getStatus() == ThingStatus.OFFLINE) {
+            // something went wrong in super.initialize() so we shouldn't initialize further here either
+            return;
+        }
+
+        Object alwaysConnectRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_ALWAYS_CONNECTED);
+        alwaysConnected = !Boolean.FALSE.equals(alwaysConnectRaw);
+
+        Object idleDisconnectDelayRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_IDLE_DISCONNECT_DELAY);
+        idleDisconnectDelay = 1000;
+        if (idleDisconnectDelayRaw instanceof Number) {
+            idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue();
+        }
+
+        if (alwaysConnected) {
+            reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
+                try {
+                    if (device.getConnectionState() != ConnectionState.CONNECTED) {
+                        if (!device.connect()) {
+                            logger.debug("Failed to connect to {}", address);
+                        }
+                        // we do not set the Thing status here, because we will anyhow receive a call to
+                        // onConnectionStateChange
+                    } else {
+                        // just in case it was already connected to begin with
+                        updateStatus(ThingStatus.ONLINE);
+                        if (!device.isServicesDiscovered() && !device.discoverServices()) {
+                            logger.debug("Error while discovering services");
+                        }
                     }
+                } catch (RuntimeException ex) {
+                    logger.warn("Unexpected error occurred", ex);
                 }
-            } catch (RuntimeException ex) {
-                logger.warn("Unexpected error occurred", ex);
-            }
-        }, 0, 30, TimeUnit.SECONDS);
+            }, 0, 30, TimeUnit.SECONDS);
+        }
     }
 
     @Override
     public void dispose() {
-        if (connectionJob != null) {
-            connectionJob.cancel(true);
-            connectionJob = null;
-        }
+        cancel(reconnectJob, true);
+        reconnectJob = null;
+        cancel(pendingDisconnect, true);
+        pendingDisconnect = null;
+
         super.dispose();
+
+        // just in case something goes really wrong in the core and it tries to dispose a handler before initializing it
+        if (scheduler != connectionTaskExecutor) {
+            connectionTaskExecutor.shutdownNow();
+        }
     }
 
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        super.handleCommand(channelUID, command);
-
-        // Handle REFRESH
-        if (command == RefreshType.REFRESH) {
-            for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
-                if (characteristic.getGattCharacteristic() != null
-                        && channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
-                    device.readCharacteristic(characteristic);
-                    break;
-                }
+    private static void cancel(@Nullable Future<?> future, boolean interrupt) {
+        if (future != null) {
+            future.cancel(interrupt);
+        }
+    }
+
+    public void connect() {
+        connectionTaskExecutor.execute(() -> {
+            if (!device.connect()) {
+                logger.debug("Failed to connect to {}", address);
+            }
+        });
+    }
+
+    public void disconnect() {
+        connectionTaskExecutor.execute(device::disconnect);
+    }
+
+    private void scheduleDisconnect() {
+        cancel(pendingDisconnect, false);
+        pendingDisconnect = connectionTaskExecutor.schedule(device::disconnect, idleDisconnectDelay,
+                TimeUnit.MILLISECONDS);
+    }
+
+    private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
+        if (device.getConnectionState() == ConnectionState.CONNECTED) {
+            return;
+        }
+        if (device.getConnectionState() != ConnectionState.CONNECTING) {
+            if (!device.connect()) {
+                throw new ConnectionException("Failed to start connecting");
+            }
+        }
+        if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
+            throw new TimeoutException("Connection attempt timeout.");
+        }
+        if (!device.isServicesDiscovered()) {
+            device.discoverServices();
+            if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
+                throw new TimeoutException("Service discovery timeout");
             }
         }
     }
 
-    @Override
-    public void channelLinked(ChannelUID channelUID) {
-        super.channelLinked(channelUID);
+    private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
+            throws BluetoothException, TimeoutException, InterruptedException {
+        connectAndWait();
+        BluetoothService service = device.getServices(serviceUUID);
+        if (service == null) {
+            throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
+        }
+        BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
+        if (characteristic == null) {
+            throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
+        }
+        return characteristic;
+    }
+
+    private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
+            Function<BluetoothCharacteristic, CompletableFuture<T>> callable) {
+        if (connectionTaskExecutor == scheduler) {
+            return CompletableFuture
+                    .failedFuture(new IllegalStateException("connectionTaskExecutor has not been initialized"));
+        }
+        if (connectionTaskExecutor.isShutdown()) {
+            return CompletableFuture.failedFuture(new IllegalStateException("connectionTaskExecutor is shut down"));
+        }
+        // we use a RetryFuture because it supports running Callable instances
+        return RetryFuture.callWithRetry(() -> {
+            // we block for completion here so that we keep the lock on the connectionTaskExecutor active.
+            return callable.apply(connectAndGetCharacteristic(serviceUUID, characteristicUUID)).get();
+        }, connectionTaskExecutor)// we make this completion async so that operations chained off the returned future
+                                  // will not run on the connectionTaskExecutor
+                .whenCompleteAsync((r, th) -> {
+                    // we us a while loop here in case the exceptions get nested
+                    while (th instanceof CompletionException || th instanceof ExecutionException) {
+                        th = th.getCause();
+                    }
+                    if (th instanceof InterruptedException) {
+                        // we don't want to schedule anything if we receive an interrupt
+                        return;
+                    }
+                    if (th instanceof TimeoutException) {
+                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, th.getMessage());
+                    }
+                    if (!alwaysConnected) {
+                        scheduleDisconnect();
+                    }
+                }, scheduler);
+    }
+
+    public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
+        return executeWithConnection(serviceUUID, characteristicUUID, device::enableNotifications);
+    }
+
+    public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
+            boolean enableNotification) {
+        var future = executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
+            if (enableNotification) {
+                return device.enableNotifications(characteristic)
+                        .thenCompose((v) -> device.writeCharacteristic(characteristic, data));
+            } else {
+                return device.writeCharacteristic(characteristic, data);
+            }
+        });
+        if (logger.isDebugEnabled()) {
+            future = future.whenComplete((v, t) -> {
+                if (t == null) {
+                    logger.debug("Characteristic {} from {} has written value {}", characteristicUUID, address,
+                            HexUtils.bytesToHex(data));
+                }
+            });
+        }
+        return future;
+    }
+
+    public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
+        var future = executeWithConnection(serviceUUID, characteristicUUID, device::readCharacteristic);
+        if (logger.isDebugEnabled()) {
+            future = future.whenComplete((data, t) -> {
+                if (t == null) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Characteristic {} from {} has been read - value {}", characteristicUUID, address,
+                                HexUtils.bytesToHex(data));
+                    }
+                }
+            });
+        }
+        return future;
     }
 
     @Override
@@ -110,10 +259,12 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
         // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
         // we are connected.
         if (receivedSignal) {
-            if (device.getConnectionState() == ConnectionState.CONNECTED) {
-                updateStatus(ThingStatus.ONLINE);
-            } else {
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
+            if (alwaysConnected) {
+                if (device.getConnectionState() == ConnectionState.CONNECTED) {
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
+                }
             }
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
@@ -126,24 +277,30 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
         switch (connectionNotification.getConnectionState()) {
             case DISCOVERED:
                 // The device is now known on the Bluetooth network, so we can do something...
-                scheduler.submit(() -> {
-                    if (device.getConnectionState() != ConnectionState.CONNECTED) {
-                        if (!device.connect()) {
-                            logger.debug("Error connecting to device after discovery.");
+                if (alwaysConnected) {
+                    connectionTaskExecutor.submit(() -> {
+                        if (device.getConnectionState() != ConnectionState.CONNECTED) {
+                            if (!device.connect()) {
+                                logger.debug("Error connecting to device after discovery.");
+                            }
                         }
-                    }
-                });
+                    });
+                }
                 break;
             case CONNECTED:
-                updateStatus(ThingStatus.ONLINE);
-                scheduler.submit(() -> {
-                    if (!resolved && !device.discoverServices()) {
-                        logger.debug("Error while discovering services");
-                    }
-                });
+                if (alwaysConnected) {
+                    connectionTaskExecutor.submit(() -> {
+                        if (!device.isServicesDiscovered() && !device.discoverServices()) {
+                            logger.debug("Error while discovering services");
+                        }
+                    });
+                }
                 break;
             case DISCONNECTED:
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+                cancel(pendingDisconnect, false);
+                if (alwaysConnected) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
+                }
                 break;
             default:
                 break;
@@ -151,51 +308,19 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
     }
 
     @Override
-    public void onServicesDiscovered() {
-        super.onServicesDiscovered();
-        if (!resolved) {
-            resolved = true;
-            logger.debug("Service discovery completed for '{}'", address);
-        }
-    }
-
-    @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        super.onCharacteristicReadComplete(characteristic, status);
-        if (status == BluetoothCompletionStatus.SUCCESS) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
-                        HexUtils.bytesToHex(characteristic.getByteValue()));
-            }
-        } else {
-            logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
-        }
-    }
-
-    @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-        super.onCharacteristicWriteComplete(characteristic, status);
-        if (logger.isDebugEnabled()) {
-            logger.debug("Wrote {} to characteristic {} of device {}: {}",
-                    HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
-        }
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-        super.onCharacteristicUpdate(characteristic);
+    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
+        super.onCharacteristicUpdate(characteristic, value);
         if (logger.isDebugEnabled()) {
-            logger.debug("Recieved update {} to characteristic {} of device {}",
-                    HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
+            logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value),
+                    characteristic.getUuid(), address);
         }
     }
 
     @Override
-    public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
-        super.onDescriptorUpdate(descriptor);
+    public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) {
+        super.onDescriptorUpdate(descriptor, value);
         if (logger.isDebugEnabled()) {
-            logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
+            logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(value),
                     descriptor.getUuid(), address);
         }
     }
diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java
new file mode 100644 (file)
index 0000000..7bf8c17
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bluetooth;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This is thrown when some kind of connection issue occurs as part of a bluetooth api call that expects a connection.
+ *
+ * @author Connor Petty - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ConnectionException extends BluetoothException {
+
+    private static final long serialVersionUID = 2966261738506666653L;
+
+    public ConnectionException() {
+        super();
+    }
+
+    public ConnectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+    public ConnectionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ConnectionException(String message) {
+        super(message);
+    }
+
+    public ConnectionException(Throwable cause) {
+        super(cause);
+    }
+}
index 074d9a8f39e7ee74cbbda3252f3cfbfe7a581555..921e61e1879c565bbcf76fb9edfad1fa6b9afdcf 100644 (file)
@@ -15,6 +15,8 @@ package org.openhab.binding.bluetooth;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -101,15 +103,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
     }
 
     @Override
-    public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
         BluetoothDevice delegate = getDelegate();
-        return delegate != null && delegate.readCharacteristic(characteristic);
+        if (delegate == null) {
+            return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
+        }
+        return delegate.readCharacteristic(characteristic);
     }
 
     @Override
-    public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
         BluetoothDevice delegate = getDelegate();
-        return delegate != null && delegate.writeCharacteristic(characteristic);
+        if (delegate == null) {
+            return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
+        }
+        return delegate.writeCharacteristic(characteristic, value);
     }
 
     @Override
@@ -119,15 +127,21 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
     }
 
     @Override
-    public boolean enableNotifications(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
         BluetoothDevice delegate = getDelegate();
-        return delegate != null ? delegate.enableNotifications(characteristic) : false;
+        if (delegate == null) {
+            return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
+        }
+        return delegate.enableNotifications(characteristic);
     }
 
     @Override
-    public boolean disableNotifications(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
         BluetoothDevice delegate = getDelegate();
-        return delegate != null ? delegate.disableNotifications(characteristic) : false;
+        if (delegate == null) {
+            return CompletableFuture.failedFuture(new IllegalStateException("Delegate is null"));
+        }
+        return delegate.disableNotifications(characteristic);
     }
 
     @Override
@@ -160,6 +174,24 @@ public abstract class DelegateBluetoothDevice extends BluetoothDevice {
         return delegate != null ? delegate.getCharacteristic(uuid) : null;
     }
 
+    @Override
+    public boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
+        BluetoothDevice delegate = getDelegate();
+        return delegate != null ? delegate.awaitConnection(timeout, unit) : false;
+    }
+
+    @Override
+    public boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
+        BluetoothDevice delegate = getDelegate();
+        return delegate != null ? delegate.awaitServiceDiscovery(timeout, unit) : false;
+    }
+
+    @Override
+    public boolean isServicesDiscovered() {
+        BluetoothDevice delegate = getDelegate();
+        return delegate != null ? delegate.isServicesDiscovered() : false;
+    }
+
     @Override
     protected void dispose() {
         BluetoothDevice delegate = getDelegate();
index c6031e770c4945bb084106c7ae8d6cb8a6b56dcf..7de03394adf431ff948926d72140ced3f7462408 100644 (file)
@@ -20,10 +20,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -34,13 +34,9 @@ import org.openhab.binding.bluetooth.BluetoothBindingConstants;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
 import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
-import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
-import org.openhab.binding.bluetooth.BluetoothDescriptor;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
-import org.openhab.binding.bluetooth.BluetoothDeviceListener;
+import org.openhab.binding.bluetooth.BluetoothUtils;
 import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
-import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
-import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
 import org.openhab.core.config.discovery.DiscoveryResult;
 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
 import org.openhab.core.thing.Thing;
@@ -56,28 +52,16 @@ import org.slf4j.LoggerFactory;
  * @author Connor Petty - Initial Contribution
  */
 @NonNullByDefault
-public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
+public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult> {
 
     private static final int DISCOVERY_TTL = 300;
 
     private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
 
-    private final Lock serviceDiscoveryLock = new ReentrantLock();
-    private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
-    private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
-    private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
-
     private final BluetoothDeviceSnapshot device;
     private final Collection<BluetoothDiscoveryParticipant> participants;
     private final Set<BluetoothAdapter> adapters;
 
-    private volatile boolean servicesDiscovered = false;
-
-    /**
-     * Contains characteristic which reading is ongoing or null if no ongoing readings.
-     */
-    private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
-
     public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
             Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
         this.participants = participants;
@@ -166,41 +150,31 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
                 .withBridge(device.getAdapter().getUID()).withLabel(label).build();
     }
 
-    // this is really just a special return type for `ensureConnected`
-    private static class ConnectionException extends Exception {
-
-    }
-
-    private void ensureConnected() throws ConnectionException, InterruptedException {
-        if (device.getConnectionState() != ConnectionState.CONNECTED) {
-            if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
-                logger.debug("Connection attempt failed to start for device {}", device.getAddress());
-                // something failed, so we abandon connection discovery
-                throw new ConnectionException();
-            }
-            if (!awaitConnection(10, TimeUnit.SECONDS)) {
-                logger.debug("Connection to device {} timed out", device.getAddress());
-                throw new ConnectionException();
-            }
-            if (!servicesDiscovered) {
-                device.discoverServices();
-                if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
-                    logger.debug("Service discovery for device {} timed out", device.getAddress());
-                    // something failed, so we abandon connection discovery
-                    throw new ConnectionException();
-                }
-            }
-            readDeviceInformationIfMissing();
-            logger.debug("Device information fetched from the device: {}", device);
-        }
-    }
-
     private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
         try {
-            device.addListener(this);
             for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
-                // we call this every time just in case a participant somehow closes the connection
-                ensureConnected();
+                if (device.getConnectionState() != ConnectionState.CONNECTED) {
+                    if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
+                        logger.debug("Connection attempt failed to start for device {}", device.getAddress());
+                        // something failed, so we abandon connection discovery
+                        return null;
+                    }
+                    if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
+                        logger.debug("Connection to device {} timed out", device.getAddress());
+                        return null;
+                    }
+                    if (!device.isServicesDiscovered()) {
+                        device.discoverServices();
+                        if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
+                            logger.debug("Service discovery for device {} timed out", device.getAddress());
+                            // something failed, so we abandon connection discovery
+                            return null;
+                        }
+                    }
+                    readDeviceInformationIfMissing();
+                    logger.debug("Device information fetched from the device: {}", device);
+                }
+
                 try {
                     DiscoveryResult result = participant.createResult(device);
                     if (result != null) {
@@ -210,180 +184,49 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
                     logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
                 }
             }
-        } catch (InterruptedException | ConnectionException e) {
+        } catch (InterruptedException e) {
             // do nothing
-        } finally {
-            device.removeListener(this);
         }
         return null;
     }
 
-    @Override
-    public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
-    }
-
-    @Override
-    public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
-        if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
-            serviceDiscoveryLock.lock();
-            try {
-                connectionCondition.signal();
-            } finally {
-                serviceDiscoveryLock.unlock();
-            }
-        }
-    }
-
     private void readDeviceInformationIfMissing() throws InterruptedException {
         if (device.getName() == null) {
-            fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
+            fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName);
         }
         if (device.getModel() == null) {
-            fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
+            fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel);
         }
         if (device.getSerialNumber() == null) {
-            fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
+            fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl);
         }
         if (device.getHardwareRevision() == null) {
-            fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
+            fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision);
         }
         if (device.getFirmwareRevision() == null) {
-            fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
+            fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision);
         }
         if (device.getSoftwareRevision() == null) {
-            fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
+            fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision);
         }
     }
 
-    private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
+    private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic, Consumer<String> consumer)
+            throws InterruptedException {
         UUID uuid = gattCharacteristic.getUUID();
         BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
         if (characteristic == null) {
             logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
             return;
         }
-        if (!device.readCharacteristic(characteristic)) {
-            logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
-            return;
-        }
-        ongoingGattCharacteristic = gattCharacteristic;
-        if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
-            logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
-            ongoingGattCharacteristic = null;
-        }
-    }
-
-    private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
-        serviceDiscoveryLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (device.getConnectionState() != ConnectionState.CONNECTED) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            serviceDiscoveryLock.unlock();
-        }
-        return true;
-    }
-
-    private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
-        serviceDiscoveryLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (ongoingGattCharacteristic != null) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            serviceDiscoveryLock.unlock();
-        }
-        return true;
-    }
-
-    private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
-        serviceDiscoveryLock.lock();
-        try {
-            long nanosTimeout = unit.toNanos(timeout);
-            while (!servicesDiscovered) {
-                if (nanosTimeout <= 0L) {
-                    return false;
-                }
-                nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
-            }
-        } finally {
-            serviceDiscoveryLock.unlock();
-        }
-        return true;
-    }
-
-    @Override
-    public void onServicesDiscovered() {
-        serviceDiscoveryLock.lock();
-        try {
-            servicesDiscovered = true;
-            serviceDiscoveryCondition.signal();
-        } finally {
-            serviceDiscoveryLock.unlock();
-        }
-    }
-
-    @Override
-    public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
-        serviceDiscoveryLock.lock();
         try {
-            if (status == BluetoothCompletionStatus.SUCCESS) {
-                switch (characteristic.getGattCharacteristic()) {
-                    case DEVICE_NAME:
-                        device.setName(characteristic.getStringValue(0));
-                        break;
-                    case MODEL_NUMBER_STRING:
-                        device.setModel(characteristic.getStringValue(0));
-                        break;
-                    case SERIAL_NUMBER_STRING:
-                        device.setSerialNumberl(characteristic.getStringValue(0));
-                        break;
-                    case HARDWARE_REVISION_STRING:
-                        device.setHardwareRevision(characteristic.getStringValue(0));
-                        break;
-                    case FIRMWARE_REVISION_STRING:
-                        device.setFirmwareRevision(characteristic.getStringValue(0));
-                        break;
-                    case SOFTWARE_REVISION_STRING:
-                        device.setSoftwareRevision(characteristic.getStringValue(0));
-                        break;
-                    default:
-                        break;
-                }
-            }
-
-            if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
-                ongoingGattCharacteristic = null;
-                infoDiscoveryCondition.signal();
-            }
-        } finally {
-            serviceDiscoveryLock.unlock();
+            byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS);
+            consumer.accept(BluetoothUtils.getStringValue(value, 0));
+        } catch (ExecutionException e) {
+            logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage());
+        } catch (TimeoutException e) {
+            logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(),
+                    e.getMessage());
         }
     }
-
-    @Override
-    public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
-            BluetoothCompletionStatus status) {
-    }
-
-    @Override
-    public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
-    }
-
-    @Override
-    public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
-    }
-
-    @Override
-    public void onAdapterChanged(BluetoothAdapter adapter) {
-    }
 }
index 892362f86c1d296b73c5e74a7c0ea13c6a7d0db2..2cf91dbe0e520691507c58aaec6cb0c8c9642a81 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.bluetooth;
 
+import java.nio.charset.StandardCharsets;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -73,14 +74,16 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
     }
 
     @Override
-    public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
+    public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
         if (characteristic.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME) {
-            characteristic.setValue(deviceName);
-            notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
-                    BluetoothCompletionStatus.SUCCESS);
-            return true;
+            String name = deviceName;
+            if (name != null) {
+                return CompletableFuture.completedFuture(name.getBytes(StandardCharsets.UTF_8));
+            } else {
+                return CompletableFuture.completedFuture(new byte[0]);
+            }
         }
-        return false;
+        return CompletableFuture.failedFuture(new UnsupportedOperationException());
     }
 
     public void setDeviceName(String deviceName) {
@@ -93,13 +96,13 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
     }
 
     @Override
-    public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
-        return false;
+    public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
+        return CompletableFuture.failedFuture(new UnsupportedOperationException());
     }
 
     @Override
-    public boolean enableNotifications(BluetoothCharacteristic characteristic) {
-        return false;
+    public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
+        return CompletableFuture.failedFuture(new UnsupportedOperationException());
     }
 
     @Override
@@ -108,8 +111,8 @@ public class MockBluetoothDevice extends BaseBluetoothDevice {
     }
 
     @Override
-    public boolean disableNotifications(BluetoothCharacteristic characteristic) {
-        return false;
+    public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
+        return CompletableFuture.failedFuture(new UnsupportedOperationException());
     }
 
     @Override