2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.bluez.internal;
16 import java.util.Objects;
17 import java.util.UUID;
18 import java.util.concurrent.CompletableFuture;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.freedesktop.dbus.errors.NoReply;
25 import org.freedesktop.dbus.exceptions.DBusException;
26 import org.freedesktop.dbus.exceptions.DBusExecutionException;
27 import org.freedesktop.dbus.types.UInt16;
28 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
29 import org.openhab.binding.bluetooth.BluetoothAddress;
30 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
31 import org.openhab.binding.bluetooth.BluetoothDescriptor;
32 import org.openhab.binding.bluetooth.BluetoothService;
33 import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
34 import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
35 import org.openhab.binding.bluetooth.bluez.internal.events.CharacteristicUpdateEvent;
36 import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
37 import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
38 import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
39 import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
40 import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
41 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
42 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
43 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
44 import org.openhab.binding.bluetooth.util.RetryException;
45 import org.openhab.binding.bluetooth.util.RetryFuture;
46 import org.openhab.core.common.ThreadPoolManager;
47 import org.openhab.core.util.HexUtils;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
51 import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
52 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic;
53 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattDescriptor;
54 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService;
57 * Implementation of BluetoothDevice for BlueZ via DBus-BlueZ API
59 * @author Kai Kreuzer - Initial contribution and API
60 * @author Benjamin Lafois - Replaced tinyB with bluezDbus
61 * @author Peter Rosenberg - Improve notifications and properties support
65 public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
67 private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
69 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
71 // Device from native lib
72 private @Nullable BluetoothDevice device = null;
77 * @param adapter the bridge handler through which this device is connected
78 * @param address the Bluetooth address of the device
79 * @param name the name of the device
81 public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
82 super(adapter, address);
83 logger.debug("Creating DBusBlueZ device with address '{}'", address);
86 @SuppressWarnings("PMD.CompareObjectsWithEquals")
87 public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
88 if (this.device != null && this.device == blueZDevice) {
91 logger.debug("updateBlueZDevice({})", blueZDevice);
93 this.device = blueZDevice;
95 if (blueZDevice == null) {
99 Short rssi = blueZDevice.getRssi();
101 this.rssi = rssi.intValue();
103 this.name = blueZDevice.getName();
104 Map<UInt16, byte[]> manData = blueZDevice.getManufacturerData();
105 if (manData != null) {
106 manData.entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
107 .ifPresent((UInt16 manufacturerId) ->
108 // Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
109 this.manufacturer = manufacturerId.intValue() & 0xFFFF);
112 if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
113 setConnectionState(ConnectionState.CONNECTED);
120 * Clean up and release memory.
123 public void dispose() {
124 BluetoothDevice dev = device;
126 if (Boolean.TRUE.equals(dev.isPaired())) {
131 dev.getAdapter().removeDevice(dev.getRawDevice());
132 } catch (DBusException ex) {
133 if (ex.getMessage().contains("Does Not Exist")) {
134 // this happens when the underlying device has already been removed
135 // but we don't have a way to check if that is the case beforehand so
136 // we will just eat the error here.
138 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
141 } catch (RuntimeException ex) {
142 // try to catch any other exceptions
143 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
149 private void setConnectionState(ConnectionState state) {
150 if (this.connectionState != state) {
151 this.connectionState = state;
152 notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
157 public boolean connect() {
158 logger.debug("Connect({})", device);
160 BluetoothDevice dev = device;
162 if (Boolean.FALSE.equals(dev.isConnected())) {
164 boolean ret = dev.connect();
165 logger.debug("Connect result: {}", ret);
167 } catch (NoReply e) {
168 // Have to double check because sometimes, exception but still worked
169 logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
170 if (Boolean.FALSE.equals(dev.isConnected())) {
172 notifyListeners(BluetoothEventType.CONNECTION_STATE,
173 new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
178 } catch (DBusExecutionException e) {
179 // Catch "software caused connection abort"
181 } catch (Exception e) {
182 logger.warn("error occured while trying to connect", e);
186 logger.debug("Device was already connected");
187 // we might be stuck in another state atm so we need to trigger a connected in this case
188 setConnectionState(ConnectionState.CONNECTED);
196 public boolean disconnect() {
197 BluetoothDevice dev = device;
199 logger.debug("Disconnecting '{}'", address);
200 return dev.disconnect();
205 private void ensureConnected() {
206 BluetoothDevice dev = device;
207 if (dev == null || !dev.isConnected()) {
208 throw new IllegalStateException("DBusBlueZ device is not set or not connected");
212 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
213 BluetoothDevice dev = device;
217 for (BluetoothGattService service : dev.getGattServices()) {
218 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
219 if (c.getUuid().equalsIgnoreCase(uuid)) {
227 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
228 BluetoothDevice dev = device;
232 for (BluetoothGattService service : dev.getGattServices()) {
233 if (dBusPath.startsWith(service.getDbusPath())) {
234 for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
235 if (dBusPath.startsWith(characteristic.getDbusPath())) {
236 return characteristic;
244 private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
245 BluetoothDevice dev = device;
249 for (BluetoothGattService service : dev.getGattServices()) {
250 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
251 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
252 if (d.getUuid().equalsIgnoreCase(uuid)) {
262 public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
263 BluetoothDevice dev = device;
264 if (dev == null || !dev.isConnected()) {
265 return CompletableFuture
266 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
269 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
271 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
272 return CompletableFuture.failedFuture(
273 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
276 return RetryFuture.callWithRetry(() -> {
279 } catch (DBusException e) {
280 if (e.getMessage().contains("Already notifying")) {
282 } else if (e.getMessage().contains("In Progress")) {
283 // let's retry in half a second
284 throw new RetryException(500, TimeUnit.MILLISECONDS);
286 logger.warn("Exception occurred while activating notifications on '{}'", address, e);
295 public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
296 logger.debug("writeCharacteristic()");
298 BluetoothDevice dev = device;
299 if (dev == null || !dev.isConnected()) {
300 return CompletableFuture
301 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
304 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
306 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
307 return CompletableFuture.failedFuture(
308 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
311 return RetryFuture.callWithRetry(() -> {
313 c.writeValue(value, null);
315 } catch (DBusException e) {
316 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
317 characteristic.getUuid(), e.getMessage());
324 public void onDBusBlueZEvent(BlueZEvent event) {
325 logger.debug("Unsupported event: {}", event);
329 public void onServicesResolved(ServicesResolvedEvent event) {
330 if (event.isResolved()) {
331 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
336 public void onNameUpdate(NameEvent event) {
337 BluetoothScanNotification notification = new BluetoothScanNotification();
338 notification.setDeviceName(event.getName());
339 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
343 public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
344 for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
345 BluetoothScanNotification notification = new BluetoothScanNotification();
346 byte[] data = new byte[entry.getValue().length + 2];
347 data[0] = (byte) (entry.getKey() & 0xFF);
348 data[1] = (byte) (entry.getKey() >>> 8);
350 System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
352 if (logger.isDebugEnabled()) {
353 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
356 notification.setManufacturerData(data);
357 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
362 public void onTxPowerUpdate(TXPowerEvent event) {
363 this.txPower = (int) event.getTxPower();
367 public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
368 // Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
369 // So we need to find the characteristic by its DBUS path.
370 BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
371 if (characteristic == null) {
372 logger.debug("Received a notification for a characteristic not found on device.");
375 BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
377 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
382 public void onRssiUpdate(RssiEvent event) {
383 int rssiTmp = event.getRssi();
385 BluetoothScanNotification notification = new BluetoothScanNotification();
386 notification.setRssi(rssiTmp);
387 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
391 public void onConnectedStatusUpdate(ConnectedEvent event) {
392 this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
393 notifyListeners(BluetoothEventType.CONNECTION_STATE,
394 new BluetoothConnectionStatusNotification(connectionState));
398 public boolean discoverServices() {
399 BluetoothDevice dev = device;
403 if (dev.getGattServices().size() > getServices().size()) {
404 for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
405 BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
406 dBusBlueZService.isPrimary());
407 for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
408 BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
409 UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
410 convertCharacteristicProperties(dBusBlueZCharacteristic, characteristic);
412 for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
413 BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
414 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
415 characteristic.addDescriptor(descriptor);
417 service.addCharacteristic(characteristic);
421 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
427 * Convert the flags of BluetoothGattCharacteristic to the int bitset used by BluetoothCharacteristic.
429 * @param dBusBlueZCharacteristic source characteristic to read the flags from
430 * @param characteristic destination characteristic to write to properties to
432 private void convertCharacteristicProperties(BluetoothGattCharacteristic dBusBlueZCharacteristic,
433 BluetoothCharacteristic characteristic) {
436 for (String property : dBusBlueZCharacteristic.getFlags()) {
439 properties |= BluetoothCharacteristic.PROPERTY_BROADCAST;
442 properties |= BluetoothCharacteristic.PROPERTY_READ;
444 case "write-without-response":
445 properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
448 properties |= BluetoothCharacteristic.PROPERTY_WRITE;
451 properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
454 properties |= BluetoothCharacteristic.PROPERTY_INDICATE;
459 characteristic.setProperties(properties);
463 public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
464 BluetoothDevice dev = device;
465 if (dev == null || !dev.isConnected()) {
466 return CompletableFuture
467 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
470 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
472 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
473 return CompletableFuture.failedFuture(
474 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
477 return RetryFuture.callWithRetry(() -> {
479 return c.readValue(null);
480 } catch (DBusException | DBusExecutionException e) {
481 // DBusExecutionException is thrown if the value cannot be read
482 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
490 public boolean isNotifying(BluetoothCharacteristic characteristic) {
491 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
493 Boolean isNotifying = c.isNotifying();
494 return Objects.requireNonNullElse(isNotifying, false);
496 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
502 public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
503 BluetoothDevice dev = device;
504 if (dev == null || !dev.isConnected()) {
505 return CompletableFuture
506 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
508 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
510 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
511 return CompletableFuture.failedFuture(
512 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
515 return RetryFuture.callWithRetry(() -> {
518 } catch (DBusException e) {
519 if (e.getMessage().contains("Already notifying")) {
521 } else if (e.getMessage().contains("In Progress")) {
522 // let's retry in half a second
523 throw new RetryException(500, TimeUnit.MILLISECONDS);
525 logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
534 public boolean enableNotifications(BluetoothDescriptor descriptor) {
535 // Not sure if it is possible to implement this
540 public boolean disableNotifications(BluetoothDescriptor descriptor) {
541 // Not sure if it is possible to implement this