2 * Copyright (c) 2010-2023 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.errors.UnknownObject;
26 import org.freedesktop.dbus.exceptions.DBusException;
27 import org.freedesktop.dbus.exceptions.DBusExecutionException;
28 import org.freedesktop.dbus.types.UInt16;
29 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
30 import org.openhab.binding.bluetooth.BluetoothAddress;
31 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
32 import org.openhab.binding.bluetooth.BluetoothDescriptor;
33 import org.openhab.binding.bluetooth.BluetoothService;
34 import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
35 import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
36 import org.openhab.binding.bluetooth.bluez.internal.events.CharacteristicUpdateEvent;
37 import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
38 import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
39 import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
40 import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
41 import org.openhab.binding.bluetooth.bluez.internal.events.ServiceDataEvent;
42 import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
43 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
44 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
45 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
46 import org.openhab.binding.bluetooth.util.RetryException;
47 import org.openhab.binding.bluetooth.util.RetryFuture;
48 import org.openhab.core.common.ThreadPoolManager;
49 import org.openhab.core.util.HexUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
53 import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
54 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic;
55 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattDescriptor;
56 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService;
59 * Implementation of BluetoothDevice for BlueZ via DBus-BlueZ API
61 * @author Kai Kreuzer - Initial contribution and API
62 * @author Benjamin Lafois - Replaced tinyB with bluezDbus
63 * @author Peter Rosenberg - Improve notifications and properties support
67 public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
69 private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
71 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
73 // Device from native lib
74 private @Nullable BluetoothDevice device = null;
79 * @param adapter the bridge handler through which this device is connected
80 * @param address the Bluetooth address of the device
82 public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
83 super(adapter, address);
84 logger.debug("Creating DBusBlueZ device with address '{}'", address);
87 @SuppressWarnings("PMD.CompareObjectsWithEquals")
88 public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
89 if (this.device != null && this.device == blueZDevice) {
92 logger.debug("updateBlueZDevice({})", blueZDevice);
94 this.device = blueZDevice;
96 if (blueZDevice == null) {
100 Short rssi = blueZDevice.getRssi();
102 this.rssi = rssi.intValue();
104 this.name = blueZDevice.getName();
105 Map<UInt16, byte[]> manData = blueZDevice.getManufacturerData();
106 if (manData != null) {
107 manData.entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
108 .ifPresent((UInt16 manufacturerId) ->
109 // Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
110 this.manufacturer = manufacturerId.intValue() & 0xFFFF);
113 if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
114 setConnectionState(ConnectionState.CONNECTED);
121 * Clean up and release memory.
124 public void dispose() {
125 BluetoothDevice dev = device;
127 if (Boolean.TRUE.equals(dev.isPaired())) {
132 dev.getAdapter().removeDevice(dev.getRawDevice());
133 } catch (DBusException ex) {
134 String exceptionMessage = ex.getMessage();
135 if (exceptionMessage == null || exceptionMessage.contains("Does Not Exist")) {
136 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
139 // this codeblock will only be hit when the underlying device has already
140 // been removed but we don't have a way to check if that is the case beforehand
141 // so we will just eat the error here.
142 } catch (RuntimeException ex) {
143 // try to catch any other exceptions
144 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
150 private void setConnectionState(ConnectionState state) {
151 if (this.connectionState != state) {
152 this.connectionState = state;
153 notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
158 public boolean connect() {
159 logger.debug("Connect({})", device);
161 BluetoothDevice dev = device;
163 if (Boolean.FALSE.equals(dev.isConnected())) {
165 boolean ret = dev.connect();
166 logger.debug("Connect result: {}", ret);
168 } catch (NoReply e) {
169 // Have to double check because sometimes, exception but still worked
170 logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
171 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);
185 logger.debug("Device was already connected");
186 // we might be stuck in another state atm so we need to trigger a connected in this case
187 setConnectionState(ConnectionState.CONNECTED);
195 public boolean disconnect() {
196 BluetoothDevice dev = device;
198 logger.debug("Disconnecting '{}'", address);
200 return dev.disconnect();
201 } catch (UnknownObject exception) {
202 logger.debug("Failed to disconnect the device, UnknownObject", exception);
209 private void ensureConnected() {
210 BluetoothDevice dev = device;
211 if (dev == null || !dev.isConnected()) {
212 throw new IllegalStateException("DBusBlueZ device is not set or not connected");
216 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
217 BluetoothDevice dev = device;
221 for (BluetoothGattService service : dev.getGattServices()) {
222 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
223 if (c.getUuid().equalsIgnoreCase(uuid)) {
231 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
232 BluetoothDevice dev = device;
236 for (BluetoothGattService service : dev.getGattServices()) {
237 if (dBusPath.startsWith(service.getDbusPath())) {
238 for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
239 if (dBusPath.startsWith(characteristic.getDbusPath())) {
240 return characteristic;
248 private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
249 BluetoothDevice dev = device;
253 for (BluetoothGattService service : dev.getGattServices()) {
254 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
255 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
256 if (d.getUuid().equalsIgnoreCase(uuid)) {
266 public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
267 BluetoothDevice dev = device;
268 if (dev == null || !dev.isConnected()) {
269 return CompletableFuture
270 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
273 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
275 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
276 return CompletableFuture.failedFuture(
277 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
280 return RetryFuture.callWithRetry(() -> {
283 } catch (DBusException e) {
284 String exceptionMessage = e.getMessage();
285 if (exceptionMessage != null && exceptionMessage.contains("Already notifying")) {
287 } else if (exceptionMessage != null && exceptionMessage.contains("In Progress")) {
288 // let's retry in half a second
289 throw new RetryException(500, TimeUnit.MILLISECONDS);
291 logger.warn("Exception occurred while activating notifications on '{}'", address, e);
300 public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
301 logger.debug("writeCharacteristic()");
303 BluetoothDevice dev = device;
304 if (dev == null || !dev.isConnected()) {
305 return CompletableFuture
306 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
309 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
311 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
312 return CompletableFuture.failedFuture(
313 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
316 return RetryFuture.callWithRetry(() -> {
318 c.writeValue(value, null);
320 } catch (DBusException e) {
321 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
322 characteristic.getUuid(), e.getMessage());
329 public void onDBusBlueZEvent(BlueZEvent event) {
330 logger.debug("Unsupported event: {}", event);
334 public void onServicesResolved(ServicesResolvedEvent event) {
335 if (event.isResolved()) {
336 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
341 public void onNameUpdate(NameEvent event) {
342 BluetoothScanNotification notification = new BluetoothScanNotification();
343 notification.setDeviceName(event.getName());
344 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
348 public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
349 for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
350 BluetoothScanNotification notification = new BluetoothScanNotification();
351 byte[] data = new byte[entry.getValue().length + 2];
352 data[0] = (byte) (entry.getKey() & 0xFF);
353 data[1] = (byte) (entry.getKey() >>> 8);
355 System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
357 if (logger.isDebugEnabled()) {
358 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
361 notification.setManufacturerData(data);
362 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
367 public void onServiceDataUpdate(ServiceDataEvent event) {
368 BluetoothScanNotification notification = new BluetoothScanNotification();
369 notification.setServiceData(event.getData());
370 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
374 public void onTxPowerUpdate(TXPowerEvent event) {
375 this.txPower = (int) event.getTxPower();
379 public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
380 // Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
381 // So we need to find the characteristic by its DBUS path.
382 BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
383 if (characteristic == null) {
384 logger.debug("Received a notification for a characteristic not found on device.");
387 BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
389 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
394 public void onRssiUpdate(RssiEvent event) {
395 int rssiTmp = event.getRssi();
397 BluetoothScanNotification notification = new BluetoothScanNotification();
398 notification.setRssi(rssiTmp);
399 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
403 public void onConnectedStatusUpdate(ConnectedEvent event) {
404 this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
405 notifyListeners(BluetoothEventType.CONNECTION_STATE,
406 new BluetoothConnectionStatusNotification(connectionState));
410 public boolean discoverServices() {
411 BluetoothDevice dev = device;
415 if (dev.getGattServices().size() > getServices().size()) {
416 for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
417 BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
418 dBusBlueZService.isPrimary());
419 for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
420 BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
421 UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
422 convertCharacteristicProperties(dBusBlueZCharacteristic, characteristic);
424 for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
425 BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
426 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
427 characteristic.addDescriptor(descriptor);
429 service.addCharacteristic(characteristic);
433 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
439 * Convert the flags of BluetoothGattCharacteristic to the int bitset used by BluetoothCharacteristic.
441 * @param dBusBlueZCharacteristic source characteristic to read the flags from
442 * @param characteristic destination characteristic to write to properties to
444 private void convertCharacteristicProperties(BluetoothGattCharacteristic dBusBlueZCharacteristic,
445 BluetoothCharacteristic characteristic) {
448 for (String property : dBusBlueZCharacteristic.getFlags()) {
451 properties |= BluetoothCharacteristic.PROPERTY_BROADCAST;
454 properties |= BluetoothCharacteristic.PROPERTY_READ;
456 case "write-without-response":
457 properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
460 properties |= BluetoothCharacteristic.PROPERTY_WRITE;
463 properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
466 properties |= BluetoothCharacteristic.PROPERTY_INDICATE;
471 characteristic.setProperties(properties);
475 public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
476 BluetoothDevice dev = device;
477 if (dev == null || !Boolean.TRUE.equals(dev.isConnected())) {
478 return CompletableFuture
479 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
482 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
484 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
485 return CompletableFuture.failedFuture(
486 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
489 return RetryFuture.callWithRetry(() -> {
491 return c.readValue(null);
492 } catch (DBusException | DBusExecutionException e) {
493 // DBusExecutionException is thrown if the value cannot be read
494 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
502 public boolean isNotifying(BluetoothCharacteristic characteristic) {
503 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
505 Boolean isNotifying = c.isNotifying();
506 return Objects.requireNonNullElse(isNotifying, false);
508 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
514 public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
515 BluetoothDevice dev = device;
516 if (dev == null || !dev.isConnected()) {
517 return CompletableFuture
518 .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
520 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
522 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
523 return CompletableFuture.failedFuture(
524 new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
527 return RetryFuture.callWithRetry(() -> {
530 } catch (DBusException e) {
531 String exceptionMessage = e.getMessage();
532 if (exceptionMessage != null && exceptionMessage.contains("Already notifying")) {
534 } else if (exceptionMessage != null && exceptionMessage.contains("In Progress")) {
535 // let's retry in half a second
536 throw new RetryException(500, TimeUnit.MILLISECONDS);
538 logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
547 public boolean enableNotifications(BluetoothDescriptor descriptor) {
548 // Not sure if it is possible to implement this
553 public boolean disableNotifications(BluetoothDescriptor descriptor) {
554 // Not sure if it is possible to implement this