]> git.basschouten.com Git - openhab-addons.git/blob
86a134823cd9d4987c2c4ce797567497c5564f0a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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  *
61  */
62 @NonNullByDefault
63 public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
64
65     private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
66
67     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
68
69     // Device from native lib
70     private @Nullable BluetoothDevice device = null;
71
72     /**
73      * Constructor
74      *
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
78      */
79     public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
80         super(adapter, address);
81         logger.debug("Creating DBusBlueZ device with address '{}'", address);
82     }
83
84     public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
85         if (this.device != null && this.device == blueZDevice) {
86             return;
87         }
88         logger.debug("updateBlueZDevice({})", blueZDevice);
89
90         this.device = blueZDevice;
91
92         if (blueZDevice == null) {
93             return;
94         }
95
96         Short rssi = blueZDevice.getRssi();
97         if (rssi != null) {
98             this.rssi = rssi.intValue();
99         }
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);
107         }
108
109         if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
110             setConnectionState(ConnectionState.CONNECTED);
111         }
112
113         discoverServices();
114     }
115
116     /**
117      * Clean up and release memory.
118      */
119     @Override
120     public void dispose() {
121         BluetoothDevice dev = device;
122         if (dev != null) {
123             if (Boolean.TRUE.equals(dev.isPaired())) {
124                 return;
125             }
126
127             try {
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.
134                 } else {
135                     logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
136                             ex.getMessage());
137                 }
138             } catch (RuntimeException ex) {
139                 // try to catch any other exceptions
140                 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
141                         ex.getMessage());
142             }
143         }
144     }
145
146     private void setConnectionState(ConnectionState state) {
147         if (this.connectionState != state) {
148             this.connectionState = state;
149             notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
150         }
151     }
152
153     @Override
154     public boolean connect() {
155         logger.debug("Connect({})", device);
156
157         BluetoothDevice dev = device;
158         if (dev != null) {
159             if (Boolean.FALSE.equals(dev.isConnected())) {
160                 try {
161                     boolean ret = dev.connect();
162                     logger.debug("Connect result: {}", ret);
163                     return 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())) {
168
169                         notifyListeners(BluetoothEventType.CONNECTION_STATE,
170                                 new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
171                         return false;
172                     } else {
173                         return true;
174                     }
175                 } catch (DBusExecutionException e) {
176                     // Catch "software caused connection abort"
177                     return false;
178                 } catch (Exception e) {
179                     logger.warn("error occured while trying to connect", e);
180                 }
181
182             } else {
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);
186                 return true;
187             }
188         }
189         return false;
190     }
191
192     @Override
193     public boolean disconnect() {
194         BluetoothDevice dev = device;
195         if (dev != null) {
196             logger.debug("Disconnecting '{}'", address);
197             return dev.disconnect();
198         }
199         return false;
200     }
201
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");
206         }
207     }
208
209     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
210         BluetoothDevice dev = device;
211         if (dev == null) {
212             return null;
213         }
214         for (BluetoothGattService service : dev.getGattServices()) {
215             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
216                 if (c.getUuid().equalsIgnoreCase(uuid)) {
217                     return c;
218                 }
219             }
220         }
221         return null;
222     }
223
224     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
225         BluetoothDevice dev = device;
226         if (dev == null) {
227             return null;
228         }
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;
234                     }
235                 }
236             }
237         }
238         return null;
239     }
240
241     private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
242         BluetoothDevice dev = device;
243         if (dev == null) {
244             return null;
245         }
246         for (BluetoothGattService service : dev.getGattServices()) {
247             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
248                 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
249                     if (d.getUuid().equalsIgnoreCase(uuid)) {
250                         return d;
251                     }
252                 }
253             }
254         }
255         return null;
256     }
257
258     @Override
259     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
260         ensureConnected();
261
262         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
263         if (c != null) {
264
265             try {
266                 c.startNotify();
267             } catch (DBusException e) {
268                 if (e.getMessage().contains("Already notifying")) {
269                     return false;
270                 } else if (e.getMessage().contains("In Progress")) {
271                     // let's retry in 10 seconds
272                     scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
273                 } else {
274                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
275                 }
276             }
277             return true;
278         } else {
279             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
280             return false;
281         }
282     }
283
284     @Override
285     public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
286         logger.debug("writeCharacteristic()");
287
288         ensureConnected();
289
290         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
291         if (c == null) {
292             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
293             return false;
294         }
295
296         scheduler.submit(() -> {
297             try {
298                 c.writeValue(characteristic.getByteValue(), null);
299                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
300                         BluetoothCompletionStatus.SUCCESS);
301
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);
307             }
308         });
309         return true;
310     }
311
312     @Override
313     public void onDBusBlueZEvent(BlueZEvent event) {
314         logger.debug("Unsupported event: {}", event);
315     }
316
317     @Override
318     public void onServicesResolved(ServicesResolvedEvent event) {
319         if (event.isResolved()) {
320             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
321         }
322     }
323
324     @Override
325     public void onNameUpdate(NameEvent event) {
326         BluetoothScanNotification notification = new BluetoothScanNotification();
327         notification.setDeviceName(event.getName());
328         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
329     }
330
331     @Override
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);
338
339             System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
340
341             if (logger.isDebugEnabled()) {
342                 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
343             }
344
345             notification.setManufacturerData(data);
346             notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
347         }
348     }
349
350     @Override
351     public void onTxPowerUpdate(TXPowerEvent event) {
352         this.txPower = (int) event.getTxPower();
353     }
354
355     @Override
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.");
362             return;
363         }
364         BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
365         if (c != null) {
366             synchronized (c) {
367                 c.setValue(event.getData());
368                 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
369             }
370         }
371     }
372
373     @Override
374     public void onRssiUpdate(RssiEvent event) {
375         int rssiTmp = event.getRssi();
376         this.rssi = rssiTmp;
377         BluetoothScanNotification notification = new BluetoothScanNotification();
378         notification.setRssi(rssiTmp);
379         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
380     }
381
382     @Override
383     public void onConnectedStatusUpdate(ConnectedEvent event) {
384         this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
385         notifyListeners(BluetoothEventType.CONNECTION_STATE,
386                 new BluetoothConnectionStatusNotification(connectionState));
387     }
388
389     @Override
390     public boolean discoverServices() {
391         BluetoothDevice dev = device;
392         if (dev == null) {
393             return false;
394         }
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);
402
403                     for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
404                         BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
405                                 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
406                         characteristic.addDescriptor(descriptor);
407                     }
408                     service.addCharacteristic(characteristic);
409                 }
410                 addService(service);
411             }
412             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
413         }
414         return true;
415     }
416
417     @Override
418     public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
419         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
420         if (c == null) {
421             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
422             return false;
423         }
424
425         scheduler.submit(() -> {
426             try {
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(),
433                         e.getMessage());
434                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
435                         BluetoothCompletionStatus.ERROR);
436             }
437         });
438         return true;
439     }
440
441     @Override
442     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
443         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
444         if (c != null) {
445             try {
446                 c.stopNotify();
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);
451                 } else {
452                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
453                 }
454             }
455             return true;
456         } else {
457             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
458             return false;
459         }
460     }
461
462     @Override
463     public boolean enableNotifications(BluetoothDescriptor descriptor) {
464         // Not sure if it is possible to implement this
465         return false;
466     }
467
468     @Override
469     public boolean disableNotifications(BluetoothDescriptor descriptor) {
470         // Not sure if it is possible to implement this
471         return false;
472     }
473 }