2 * Copyright (c) 2010-2020 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.ScheduledExecutorService;
19 import java.util.concurrent.TimeUnit;
21 import org.bluez.exceptions.BluezFailedException;
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.BluetoothCompletionStatus;
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.ServicesResolvedEvent;
42 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
43 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
44 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
45 import org.openhab.core.common.ThreadPoolManager;
46 import org.openhab.core.util.HexUtils;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
51 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic;
52 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattDescriptor;
53 import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService;
56 * Implementation of BluetoothDevice for BlueZ via DBus-BlueZ API
58 * @author Kai Kreuzer - Initial contribution and API
59 * @author Benjamin Lafois - Replaced tinyB with bluezDbus
63 public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
65 private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
67 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
69 // Device from native lib
70 private @Nullable BluetoothDevice device = null;
75 * @param adapter the bridge handler through which this device is connected
76 * @param address the Bluetooth address of the device
77 * @param name the name of the device
79 public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
80 super(adapter, address);
81 logger.debug("Creating DBusBlueZ device with address '{}'", address);
84 public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
85 if (this.device != null && this.device == blueZDevice) {
88 logger.debug("updateBlueZDevice({})", blueZDevice);
90 this.device = blueZDevice;
92 if (blueZDevice == null) {
96 Short rssi = blueZDevice.getRssi();
98 this.rssi = rssi.intValue();
100 this.name = blueZDevice.getName();
101 Map<UInt16, byte[]> manData = blueZDevice.getManufacturerData();
102 if (manData != null) {
103 manData.entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
104 .ifPresent((UInt16 manufacturerId) ->
105 // Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
106 this.manufacturer = manufacturerId.intValue() & 0xFFFF);
109 if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
110 setConnectionState(ConnectionState.CONNECTED);
117 * Clean up and release memory.
120 public void dispose() {
121 BluetoothDevice dev = device;
123 if (Boolean.TRUE.equals(dev.isPaired())) {
128 dev.getAdapter().removeDevice(dev.getRawDevice());
129 } catch (DBusException ex) {
130 if (ex.getMessage().contains("Does Not Exist")) {
131 // this happens when the underlying device has already been removed
132 // but we don't have a way to check if that is the case beforehand so
133 // we will just eat the error here.
135 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
138 } catch (RuntimeException ex) {
139 // try to catch any other exceptions
140 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
146 private void setConnectionState(ConnectionState state) {
147 if (this.connectionState != state) {
148 this.connectionState = state;
149 notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
154 public boolean connect() {
155 logger.debug("Connect({})", device);
157 BluetoothDevice dev = device;
159 if (Boolean.FALSE.equals(dev.isConnected())) {
161 boolean ret = dev.connect();
162 logger.debug("Connect result: {}", ret);
164 } catch (NoReply e) {
165 // Have to double check because sometimes, exception but still worked
166 logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
167 if (Boolean.FALSE.equals(dev.isConnected())) {
169 notifyListeners(BluetoothEventType.CONNECTION_STATE,
170 new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
175 } catch (DBusExecutionException e) {
176 // Catch "software caused connection abort"
178 } catch (Exception e) {
179 logger.warn("error occured while trying to connect", e);
183 logger.debug("Device was already connected");
184 // we might be stuck in another state atm so we need to trigger a connected in this case
185 setConnectionState(ConnectionState.CONNECTED);
193 public boolean disconnect() {
194 BluetoothDevice dev = device;
196 logger.debug("Disconnecting '{}'", address);
197 return dev.disconnect();
202 private void ensureConnected() {
203 BluetoothDevice dev = device;
204 if (dev == null || !dev.isConnected()) {
205 throw new IllegalStateException("DBusBlueZ device is not set or not connected");
209 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
210 BluetoothDevice dev = device;
214 for (BluetoothGattService service : dev.getGattServices()) {
215 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
216 if (c.getUuid().equalsIgnoreCase(uuid)) {
224 private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
225 BluetoothDevice dev = device;
229 for (BluetoothGattService service : dev.getGattServices()) {
230 if (dBusPath.startsWith(service.getDbusPath())) {
231 for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
232 if (dBusPath.startsWith(characteristic.getDbusPath())) {
233 return characteristic;
241 private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
242 BluetoothDevice dev = device;
246 for (BluetoothGattService service : dev.getGattServices()) {
247 for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
248 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
249 if (d.getUuid().equalsIgnoreCase(uuid)) {
259 public boolean enableNotifications(BluetoothCharacteristic characteristic) {
262 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
267 } catch (DBusException e) {
268 if (e.getMessage().contains("Already notifying")) {
270 } else if (e.getMessage().contains("In Progress")) {
271 // let's retry in 10 seconds
272 scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
274 logger.warn("Exception occurred while activating notifications on '{}'", address, e);
279 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
285 public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
286 logger.debug("writeCharacteristic()");
290 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
292 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
296 scheduler.submit(() -> {
298 c.writeValue(characteristic.getByteValue(), null);
299 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
300 BluetoothCompletionStatus.SUCCESS);
302 } catch (DBusException e) {
303 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
304 characteristic.getUuid(), e.getMessage());
305 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
306 BluetoothCompletionStatus.ERROR);
313 public void onDBusBlueZEvent(BlueZEvent event) {
314 logger.debug("Unsupported event: {}", event);
318 public void onServicesResolved(ServicesResolvedEvent event) {
319 if (event.isResolved()) {
320 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
325 public void onNameUpdate(NameEvent event) {
326 BluetoothScanNotification notification = new BluetoothScanNotification();
327 notification.setDeviceName(event.getName());
328 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
332 public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
333 for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
334 BluetoothScanNotification notification = new BluetoothScanNotification();
335 byte[] data = new byte[entry.getValue().length + 2];
336 data[0] = (byte) (entry.getKey() & 0xFF);
337 data[1] = (byte) (entry.getKey() >>> 8);
339 System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
341 if (logger.isDebugEnabled()) {
342 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
345 notification.setManufacturerData(data);
346 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
351 public void onTxPowerUpdate(TXPowerEvent event) {
352 this.txPower = (int) event.getTxPower();
356 public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
357 // Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
358 // So we need to find the characteristic by its DBUS path.
359 BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
360 if (characteristic == null) {
361 logger.debug("Received a notification for a characteristic not found on device.");
364 BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
367 c.setValue(event.getData());
368 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
374 public void onRssiUpdate(RssiEvent event) {
375 int rssiTmp = event.getRssi();
377 BluetoothScanNotification notification = new BluetoothScanNotification();
378 notification.setRssi(rssiTmp);
379 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
383 public void onConnectedStatusUpdate(ConnectedEvent event) {
384 this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
385 notifyListeners(BluetoothEventType.CONNECTION_STATE,
386 new BluetoothConnectionStatusNotification(connectionState));
390 public boolean discoverServices() {
391 BluetoothDevice dev = device;
395 if (dev.getGattServices().size() > getServices().size()) {
396 for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
397 BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
398 dBusBlueZService.isPrimary());
399 for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
400 BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
401 UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
403 for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
404 BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
405 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
406 characteristic.addDescriptor(descriptor);
408 service.addCharacteristic(characteristic);
412 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
418 public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
419 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
421 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
425 scheduler.submit(() -> {
427 byte[] value = c.readValue(null);
428 characteristic.setValue(value);
429 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
430 BluetoothCompletionStatus.SUCCESS);
431 } catch (DBusException e) {
432 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
434 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
435 BluetoothCompletionStatus.ERROR);
442 public boolean disableNotifications(BluetoothCharacteristic characteristic) {
443 BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
447 } catch (BluezFailedException e) {
448 if (e.getMessage().contains("In Progress")) {
449 // let's retry in 10 seconds
450 scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
452 logger.warn("Exception occurred while activating notifications on '{}'", address, e);
457 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
463 public boolean enableNotifications(BluetoothDescriptor descriptor) {
464 // Not sure if it is possible to implement this
469 public boolean disableNotifications(BluetoothDescriptor descriptor) {
470 // Not sure if it is possible to implement this