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