From 89d735bb0f312cbc5097a394e0ec438b0dcb815f Mon Sep 17 00:00:00 2001 From: Connor Petty Date: Fri, 9 Apr 2021 13:23:28 -0700 Subject: [PATCH] [bluetooth] Changed characteristic read/write to use CompletableFutures (#8970) Signed-off-by: Connor Petty --- .../internal/AirthingsWavePlusHandler.java | 55 +- .../bluetooth/am43/internal/AM43Handler.java | 49 +- .../bluegiga/BlueGigaBluetoothDevice.java | 334 ++++++++----- .../bluez/internal/BlueZBluetoothDevice.java | 153 +++--- .../handler/DaikinMadokaHandler.java | 72 +-- .../internal/GenericBluetoothHandler.java | 62 ++- .../main/resources/OH-INF/thing/generic.xml | 5 + .../internal/ConnectedBluetoothHandler.java | 472 ------------------ .../internal/GoveeHygrometerHandler.java | 16 +- .../internal/RoamingBluetoothDevice.java | 25 +- .../bluetooth/BaseBluetoothDevice.java | 70 +++ .../bluetooth/BeaconBluetoothHandler.java | 21 +- .../bluetooth/BluetoothBindingConstants.java | 2 + .../bluetooth/BluetoothCharacteristic.java | 308 ------------ .../bluetooth/BluetoothDescriptor.java | 20 +- .../binding/bluetooth/BluetoothDevice.java | 47 +- .../bluetooth/BluetoothDeviceListener.java | 24 +- .../binding/bluetooth/BluetoothException.java | 48 ++ .../binding/bluetooth/BluetoothUtils.java | 292 +++++++++++ .../bluetooth/ConnectedBluetoothHandler.java | 329 ++++++++---- .../bluetooth/ConnectionException.java | 47 ++ .../bluetooth/DelegateBluetoothDevice.java | 48 +- .../internal/BluetoothDiscoveryProcess.java | 243 ++------- .../bluetooth/MockBluetoothDevice.java | 27 +- 24 files changed, 1207 insertions(+), 1562 deletions(-) delete mode 100644 bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java create mode 100644 bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java create mode 100644 bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java create mode 100644 bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java index f31aa5336..4cac35518 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java @@ -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)); diff --git a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java index 2588dd062..c0be052e3 100644 --- a/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java +++ b/bundles/org.openhab.binding.bluetooth.am43/src/main/java/org/openhab/binding/bluetooth/am43/internal/AM43Handler.java @@ -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) { diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java index ccff259a0..3fce5a0e4 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java @@ -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 handleToUUID = new HashMap<>(); private NavigableMap 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 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); + notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor, + BluetoothUtils.toByteArray(event.getValue())); } } - private static byte @Nullable [] toBytes(int @Nullable [] 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]; - } - 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 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; + } + } } diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java index 509f21fa4..4f865323a 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java @@ -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(); - - BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString()); - if (c != null) { - - try { - c.startNotify(); - } catch (DBusException e) { - if (e.getMessage().contains("Already notifying")) { - return false; - } else if (e.getMessage().contains("In Progress")) { - // let's retry in 10 seconds - scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS); - } else { - logger.warn("Exception occurred while activating notifications on '{}'", address, e); - } - } - return true; - } else { - logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address); - return false; + 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")); } - } - - @Override - public boolean writeCharacteristic(BluetoothCharacteristic characteristic) { - logger.debug("writeCharacteristic()"); - - ensureConnected(); 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.startNotify(); + } 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); + throw e; + } + } + return null; + }, scheduler); + } + @Override + public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) { + logger.debug("writeCharacteristic()"); + + 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 CompletableFuture.failedFuture( + new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device")); + } + + return RetryFuture.callWithRetry(() -> { + try { + 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 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 diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java index e8169113b..d1a2ba510 100644 --- a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java @@ -12,13 +12,14 @@ */ 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. diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java index 633170f42..37a594e4d 100644 --- a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java @@ -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, diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml index eef31338d..a4f76990c 100644 --- a/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/resources/OH-INF/thing/generic.xml @@ -25,6 +25,11 @@ The frequency at which readable characteristics refreshed 30 + + + If enabled, will automatically connect to the device and reconnect if connection is lost. + true + 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 index a6f3bdbe6..000000000 --- a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/ConnectedBluetoothHandler.java +++ /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 CompletableFuture executeWithConnection(UUID serviceUUID, UUID characteristicUUID, - CallableFunction callable) { - CompletableFuture 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 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 { - public R call(U arg) throws Exception; - } -} diff --git a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java index 21caa88cf..6072c3a6d 100644 --- a/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java +++ b/bundles/org.openhab.binding.bluetooth.govee/src/main/java/org/openhab/binding/bluetooth/govee/internal/GoveeHygrometerHandler.java @@ -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 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 { diff --git a/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java index 92f3af7b7..79cbcfd93 100644 --- a/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.roaming/src/main/java/org/openhab/binding/bluetooth/roaming/internal/RoamingBluetoothDevice.java @@ -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); } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java index 210d43213..c6422cc74 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothDevice.java @@ -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 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); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java index c1602e52d..ac39941c8 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java @@ -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(); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java index 9e8a591bc..77509f155 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothBindingConstants.java @@ -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; diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java index dd591acc7..b452997df 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothCharacteristic.java @@ -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); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java index ec6c131ca..c3afcc08b 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDescriptor.java @@ -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); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java index ba1186829..a1616f9ae 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java @@ -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. *

- * This is an asynchronous method. Once the read is complete - * {@link BluetoothDeviceListener.onCharacteristicReadComplete} - * method will be called with the completion state. - *

- * 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 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. *

- * 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 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]); diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java index 858ac0ef2..4220cd8e1 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDeviceListener.java @@ -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 index 000000000..d4977a1c5 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothException.java @@ -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 index 000000000..454a56b35 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothUtils.java @@ -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; + } + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java index 12ea302d5..556b23ee2 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java @@ -12,21 +12,26 @@ */ 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 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); + private static void cancel(@Nullable Future future, boolean interrupt) { + if (future != null) { + future.cancel(interrupt); + } + } - // Handle REFRESH - if (command == RefreshType.REFRESH) { - for (BluetoothCharacteristic characteristic : deviceCharacteristics) { - if (characteristic.getGattCharacteristic() != null - && channelUID.getId().equals(characteristic.getGattCharacteristic().name())) { - device.readCharacteristic(characteristic); - break; - } + 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 CompletableFuture executeWithConnection(UUID serviceUUID, UUID characteristicUUID, + Function> 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 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); + public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) { + super.onCharacteristicUpdate(characteristic, value); if (logger.isDebugEnabled()) { - logger.debug("Wrote {} to characteristic {} of device {}: {}", - HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status); + logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value), + characteristic.getUuid(), address); } } @Override - public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { - super.onCharacteristicUpdate(characteristic); + public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) { + super.onDescriptorUpdate(descriptor, value); 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()), + 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 index 000000000..7bf8c17b5 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectionException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java index 074d9a8f3..921e61e18 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/DelegateBluetoothDevice.java @@ -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 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(); diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java index c6031e770..7de03394a 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java @@ -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, BluetoothDeviceListener { +public class BluetoothDiscoveryProcess implements Supplier { 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 participants; private final Set 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 participants, Set adapters) { this.participants = participants; @@ -166,41 +150,31 @@ public class BluetoothDiscoveryProcess implements Supplier, 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 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, 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 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(); + 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()); } - 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(); - } - } - - @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) { } } diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java index 892362f86..2cf91dbe0 100644 --- a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothDevice.java @@ -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 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