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