]> git.basschouten.com Git - openhab-addons.git/blob
4f865323a26d465ea7ba4fcbfba1ef43771db4e5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth.bluez.internal;
14
15 import java.util.Map;
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;
21
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;
50
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;
55
56 /**
57  * Implementation of BluetoothDevice for BlueZ via DBus-BlueZ API
58  *
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
62  *
63  */
64 @NonNullByDefault
65 public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
66
67     private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
68
69     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
70
71     // Device from native lib
72     private @Nullable BluetoothDevice device = null;
73
74     /**
75      * Constructor
76      *
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
80      */
81     public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
82         super(adapter, address);
83         logger.debug("Creating DBusBlueZ device with address '{}'", address);
84     }
85
86     public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
87         if (this.device != null && this.device == blueZDevice) {
88             return;
89         }
90         logger.debug("updateBlueZDevice({})", blueZDevice);
91
92         this.device = blueZDevice;
93
94         if (blueZDevice == null) {
95             return;
96         }
97
98         Short rssi = blueZDevice.getRssi();
99         if (rssi != null) {
100             this.rssi = rssi.intValue();
101         }
102         this.name = blueZDevice.getName();
103         Map<UInt16, byte[]> manData = blueZDevice.getManufacturerData();
104         if (manData != null) {
105             manData.entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
106                     .ifPresent((UInt16 manufacturerId) ->
107                     // Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
108                     this.manufacturer = manufacturerId.intValue() & 0xFFFF);
109         }
110
111         if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
112             setConnectionState(ConnectionState.CONNECTED);
113         }
114
115         discoverServices();
116     }
117
118     /**
119      * Clean up and release memory.
120      */
121     @Override
122     public void dispose() {
123         BluetoothDevice dev = device;
124         if (dev != null) {
125             if (Boolean.TRUE.equals(dev.isPaired())) {
126                 return;
127             }
128
129             try {
130                 dev.getAdapter().removeDevice(dev.getRawDevice());
131             } catch (DBusException ex) {
132                 if (ex.getMessage().contains("Does Not Exist")) {
133                     // this happens when the underlying device has already been removed
134                     // but we don't have a way to check if that is the case beforehand so
135                     // we will just eat the error here.
136                 } else {
137                     logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
138                             ex.getMessage());
139                 }
140             } catch (RuntimeException ex) {
141                 // try to catch any other exceptions
142                 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
143                         ex.getMessage());
144             }
145         }
146     }
147
148     private void setConnectionState(ConnectionState state) {
149         if (this.connectionState != state) {
150             this.connectionState = state;
151             notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
152         }
153     }
154
155     @Override
156     public boolean connect() {
157         logger.debug("Connect({})", device);
158
159         BluetoothDevice dev = device;
160         if (dev != null) {
161             if (Boolean.FALSE.equals(dev.isConnected())) {
162                 try {
163                     boolean ret = dev.connect();
164                     logger.debug("Connect result: {}", ret);
165                     return ret;
166                 } catch (NoReply e) {
167                     // Have to double check because sometimes, exception but still worked
168                     logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
169                     if (Boolean.FALSE.equals(dev.isConnected())) {
170
171                         notifyListeners(BluetoothEventType.CONNECTION_STATE,
172                                 new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
173                         return false;
174                     } else {
175                         return true;
176                     }
177                 } catch (DBusExecutionException e) {
178                     // Catch "software caused connection abort"
179                     return false;
180                 } catch (Exception e) {
181                     logger.warn("error occured while trying to connect", e);
182                 }
183
184             } else {
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);
188                 return true;
189             }
190         }
191         return false;
192     }
193
194     @Override
195     public boolean disconnect() {
196         BluetoothDevice dev = device;
197         if (dev != null) {
198             logger.debug("Disconnecting '{}'", address);
199             return dev.disconnect();
200         }
201         return false;
202     }
203
204     private void ensureConnected() {
205         BluetoothDevice dev = device;
206         if (dev == null || !dev.isConnected()) {
207             throw new IllegalStateException("DBusBlueZ device is not set or not connected");
208         }
209     }
210
211     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
212         BluetoothDevice dev = device;
213         if (dev == null) {
214             return null;
215         }
216         for (BluetoothGattService service : dev.getGattServices()) {
217             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
218                 if (c.getUuid().equalsIgnoreCase(uuid)) {
219                     return c;
220                 }
221             }
222         }
223         return null;
224     }
225
226     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
227         BluetoothDevice dev = device;
228         if (dev == null) {
229             return null;
230         }
231         for (BluetoothGattService service : dev.getGattServices()) {
232             if (dBusPath.startsWith(service.getDbusPath())) {
233                 for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
234                     if (dBusPath.startsWith(characteristic.getDbusPath())) {
235                         return characteristic;
236                     }
237                 }
238             }
239         }
240         return null;
241     }
242
243     private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
244         BluetoothDevice dev = device;
245         if (dev == null) {
246             return null;
247         }
248         for (BluetoothGattService service : dev.getGattServices()) {
249             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
250                 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
251                     if (d.getUuid().equalsIgnoreCase(uuid)) {
252                         return d;
253                     }
254                 }
255             }
256         }
257         return null;
258     }
259
260     @Override
261     public CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic) {
262         BluetoothDevice dev = device;
263         if (dev == null || !dev.isConnected()) {
264             return CompletableFuture
265                     .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
266         }
267
268         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
269         if (c == null) {
270             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
271             return CompletableFuture.failedFuture(
272                     new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
273         }
274
275         return RetryFuture.callWithRetry(() -> {
276             try {
277                 c.startNotify();
278             } catch (DBusException e) {
279                 if (e.getMessage().contains("Already notifying")) {
280                     return null;
281                 } else if (e.getMessage().contains("In Progress")) {
282                     // let's retry in half a second
283                     throw new RetryException(500, TimeUnit.MILLISECONDS);
284                 } else {
285                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
286                     throw e;
287                 }
288             }
289             return null;
290         }, scheduler);
291     }
292
293     @Override
294     public CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic, byte[] value) {
295         logger.debug("writeCharacteristic()");
296
297         BluetoothDevice dev = device;
298         if (dev == null || !dev.isConnected()) {
299             return CompletableFuture
300                     .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
301         }
302
303         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
304         if (c == null) {
305             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
306             return CompletableFuture.failedFuture(
307                     new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
308         }
309
310         return RetryFuture.callWithRetry(() -> {
311             try {
312                 c.writeValue(value, null);
313                 return null;
314             } catch (DBusException e) {
315                 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
316                         characteristic.getUuid(), e.getMessage());
317                 throw e;
318             }
319         }, scheduler);
320     }
321
322     @Override
323     public void onDBusBlueZEvent(BlueZEvent event) {
324         logger.debug("Unsupported event: {}", event);
325     }
326
327     @Override
328     public void onServicesResolved(ServicesResolvedEvent event) {
329         if (event.isResolved()) {
330             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
331         }
332     }
333
334     @Override
335     public void onNameUpdate(NameEvent event) {
336         BluetoothScanNotification notification = new BluetoothScanNotification();
337         notification.setDeviceName(event.getName());
338         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
339     }
340
341     @Override
342     public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
343         for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
344             BluetoothScanNotification notification = new BluetoothScanNotification();
345             byte[] data = new byte[entry.getValue().length + 2];
346             data[0] = (byte) (entry.getKey() & 0xFF);
347             data[1] = (byte) (entry.getKey() >>> 8);
348
349             System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
350
351             if (logger.isDebugEnabled()) {
352                 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
353             }
354
355             notification.setManufacturerData(data);
356             notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
357         }
358     }
359
360     @Override
361     public void onTxPowerUpdate(TXPowerEvent event) {
362         this.txPower = (int) event.getTxPower();
363     }
364
365     @Override
366     public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
367         // Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
368         // So we need to find the characteristic by its DBUS path.
369         BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
370         if (characteristic == null) {
371             logger.debug("Received a notification for a characteristic not found on device.");
372             return;
373         }
374         BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
375         if (c != null) {
376             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, event.getData());
377         }
378     }
379
380     @Override
381     public void onRssiUpdate(RssiEvent event) {
382         int rssiTmp = event.getRssi();
383         this.rssi = rssiTmp;
384         BluetoothScanNotification notification = new BluetoothScanNotification();
385         notification.setRssi(rssiTmp);
386         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
387     }
388
389     @Override
390     public void onConnectedStatusUpdate(ConnectedEvent event) {
391         this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
392         notifyListeners(BluetoothEventType.CONNECTION_STATE,
393                 new BluetoothConnectionStatusNotification(connectionState));
394     }
395
396     @Override
397     public boolean discoverServices() {
398         BluetoothDevice dev = device;
399         if (dev == null) {
400             return false;
401         }
402         if (dev.getGattServices().size() > getServices().size()) {
403             for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
404                 BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
405                         dBusBlueZService.isPrimary());
406                 for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
407                     BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
408                             UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
409                     convertCharacteristicProperties(dBusBlueZCharacteristic, characteristic);
410
411                     for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
412                         BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
413                                 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
414                         characteristic.addDescriptor(descriptor);
415                     }
416                     service.addCharacteristic(characteristic);
417                 }
418                 addService(service);
419             }
420             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
421         }
422         return true;
423     }
424
425     /**
426      * Convert the flags of BluetoothGattCharacteristic to the int bitset used by BluetoothCharacteristic.
427      *
428      * @param dBusBlueZCharacteristic source characteristic to read the flags from
429      * @param characteristic destination characteristic to write to properties to
430      */
431     private void convertCharacteristicProperties(BluetoothGattCharacteristic dBusBlueZCharacteristic,
432             BluetoothCharacteristic characteristic) {
433         int properties = 0;
434
435         for (String property : dBusBlueZCharacteristic.getFlags()) {
436             switch (property) {
437                 case "broadcast":
438                     properties |= BluetoothCharacteristic.PROPERTY_BROADCAST;
439                     break;
440                 case "read":
441                     properties |= BluetoothCharacteristic.PROPERTY_READ;
442                     break;
443                 case "write-without-response":
444                     properties |= BluetoothCharacteristic.PROPERTY_WRITE_NO_RESPONSE;
445                     break;
446                 case "write":
447                     properties |= BluetoothCharacteristic.PROPERTY_WRITE;
448                     break;
449                 case "notify":
450                     properties |= BluetoothCharacteristic.PROPERTY_NOTIFY;
451                     break;
452                 case "indicate":
453                     properties |= BluetoothCharacteristic.PROPERTY_INDICATE;
454                     break;
455             }
456         }
457
458         characteristic.setProperties(properties);
459     }
460
461     @Override
462     public CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic) {
463         BluetoothDevice dev = device;
464         if (dev == null || !dev.isConnected()) {
465             return CompletableFuture
466                     .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
467         }
468
469         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
470         if (c == null) {
471             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
472             return CompletableFuture.failedFuture(
473                     new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
474         }
475
476         return RetryFuture.callWithRetry(() -> {
477             try {
478                 return c.readValue(null);
479             } catch (DBusException | DBusExecutionException e) {
480                 // DBusExecutionException is thrown if the value cannot be read
481                 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
482                         e.getMessage());
483                 throw e;
484             }
485         }, scheduler);
486     }
487
488     @Override
489     public boolean isNotifying(BluetoothCharacteristic characteristic) {
490         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
491         if (c != null) {
492             Boolean isNotifying = c.isNotifying();
493             return Objects.requireNonNullElse(isNotifying, false);
494         } else {
495             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
496             return false;
497         }
498     }
499
500     @Override
501     public CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic) {
502         BluetoothDevice dev = device;
503         if (dev == null || !dev.isConnected()) {
504             return CompletableFuture
505                     .failedFuture(new IllegalStateException("DBusBlueZ device is not set or not connected"));
506         }
507         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
508         if (c == null) {
509             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
510             return CompletableFuture.failedFuture(
511                     new IllegalStateException("Characteristic " + characteristic.getUuid() + " is missing on device"));
512         }
513
514         return RetryFuture.callWithRetry(() -> {
515             try {
516                 c.stopNotify();
517             } catch (DBusException e) {
518                 if (e.getMessage().contains("Already notifying")) {
519                     return null;
520                 } else if (e.getMessage().contains("In Progress")) {
521                     // let's retry in half a second
522                     throw new RetryException(500, TimeUnit.MILLISECONDS);
523                 } else {
524                     logger.warn("Exception occurred while deactivating notifications on '{}'", address, e);
525                     throw e;
526                 }
527             }
528             return null;
529         }, scheduler);
530     }
531
532     @Override
533     public boolean enableNotifications(BluetoothDescriptor descriptor) {
534         // Not sure if it is possible to implement this
535         return false;
536     }
537
538     @Override
539     public boolean disableNotifications(BluetoothDescriptor descriptor) {
540         // Not sure if it is possible to implement this
541         return false;
542     }
543 }