]> git.basschouten.com Git - openhab-addons.git/blob
af80f79d2f12a77fa67eaf27e706a371fb7c7622
[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             try {
124                 dev.getAdapter().removeDevice(dev.getRawDevice());
125             } catch (DBusException ex) {
126                 if (ex.getMessage().contains("Does Not Exist")) {
127                     // this happens when the underlying device has already been removed
128                     // but we don't have a way to check if that is the case beforehand so
129                     // we will just eat the error here.
130                 } else {
131                     logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
132                             ex.getMessage());
133                 }
134             } catch (RuntimeException ex) {
135                 // try to catch any other exceptions
136                 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
137                         ex.getMessage());
138             }
139         }
140     }
141
142     private void setConnectionState(ConnectionState state) {
143         if (this.connectionState != state) {
144             this.connectionState = state;
145             notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
146         }
147     }
148
149     @Override
150     public boolean connect() {
151         logger.debug("Connect({})", device);
152
153         BluetoothDevice dev = device;
154         if (dev != null) {
155             if (Boolean.FALSE.equals(dev.isConnected())) {
156                 try {
157                     boolean ret = dev.connect();
158                     logger.debug("Connect result: {}", ret);
159                     return ret;
160                 } catch (NoReply e) {
161                     // Have to double check because sometimes, exception but still worked
162                     logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
163                     if (Boolean.FALSE.equals(dev.isConnected())) {
164
165                         notifyListeners(BluetoothEventType.CONNECTION_STATE,
166                                 new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
167                         return false;
168                     } else {
169                         return true;
170                     }
171                 } catch (DBusExecutionException e) {
172                     // Catch "software caused connection abort"
173                     return false;
174                 } catch (Exception e) {
175                     logger.warn("error occured while trying to connect", e);
176                 }
177
178             } else {
179                 logger.debug("Device was already connected");
180                 // we might be stuck in another state atm so we need to trigger a connected in this case
181                 setConnectionState(ConnectionState.CONNECTED);
182                 return true;
183             }
184         }
185         return false;
186     }
187
188     @Override
189     public boolean disconnect() {
190         BluetoothDevice dev = device;
191         if (dev != null) {
192             logger.debug("Disconnecting '{}'", address);
193             return dev.disconnect();
194         }
195         return false;
196     }
197
198     private void ensureConnected() {
199         BluetoothDevice dev = device;
200         if (dev == null || !dev.isConnected()) {
201             throw new IllegalStateException("DBusBlueZ device is not set or not connected");
202         }
203     }
204
205     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
206         BluetoothDevice dev = device;
207         if (dev == null) {
208             return null;
209         }
210         for (BluetoothGattService service : dev.getGattServices()) {
211             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
212                 if (c.getUuid().equalsIgnoreCase(uuid)) {
213                     return c;
214                 }
215             }
216         }
217         return null;
218     }
219
220     private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
221         BluetoothDevice dev = device;
222         if (dev == null) {
223             return null;
224         }
225         for (BluetoothGattService service : dev.getGattServices()) {
226             if (dBusPath.startsWith(service.getDbusPath())) {
227                 for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
228                     if (dBusPath.startsWith(characteristic.getDbusPath())) {
229                         return characteristic;
230                     }
231                 }
232             }
233         }
234         return null;
235     }
236
237     private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
238         BluetoothDevice dev = device;
239         if (dev == null) {
240             return null;
241         }
242         for (BluetoothGattService service : dev.getGattServices()) {
243             for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
244                 for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
245                     if (d.getUuid().equalsIgnoreCase(uuid)) {
246                         return d;
247                     }
248                 }
249             }
250         }
251         return null;
252     }
253
254     @Override
255     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
256         ensureConnected();
257
258         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
259         if (c != null) {
260
261             try {
262                 c.startNotify();
263             } catch (DBusException e) {
264                 if (e.getMessage().contains("Already notifying")) {
265                     return false;
266                 } else if (e.getMessage().contains("In Progress")) {
267                     // let's retry in 10 seconds
268                     scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
269                 } else {
270                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
271                 }
272             }
273             return true;
274         } else {
275             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
276             return false;
277         }
278     }
279
280     @Override
281     public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
282         logger.debug("writeCharacteristic()");
283
284         ensureConnected();
285
286         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
287         if (c == null) {
288             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
289             return false;
290         }
291
292         scheduler.submit(() -> {
293             try {
294                 c.writeValue(characteristic.getByteValue(), null);
295                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
296                         BluetoothCompletionStatus.SUCCESS);
297
298             } catch (DBusException e) {
299                 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
300                         characteristic.getUuid(), e.getMessage());
301                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
302                         BluetoothCompletionStatus.ERROR);
303             }
304         });
305         return true;
306     }
307
308     @Override
309     public void onDBusBlueZEvent(BlueZEvent event) {
310         logger.debug("Unsupported event: {}", event);
311     }
312
313     @Override
314     public void onServicesResolved(ServicesResolvedEvent event) {
315         if (event.isResolved()) {
316             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
317         }
318     }
319
320     @Override
321     public void onNameUpdate(NameEvent event) {
322         BluetoothScanNotification notification = new BluetoothScanNotification();
323         notification.setDeviceName(event.getName());
324         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
325     }
326
327     @Override
328     public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
329         for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
330             BluetoothScanNotification notification = new BluetoothScanNotification();
331             byte[] data = new byte[entry.getValue().length + 2];
332             data[0] = (byte) (entry.getKey() & 0xFF);
333             data[1] = (byte) (entry.getKey() >>> 8);
334
335             System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
336
337             if (logger.isDebugEnabled()) {
338                 logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
339             }
340
341             notification.setManufacturerData(data);
342             notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
343         }
344     }
345
346     @Override
347     public void onTxPowerUpdate(TXPowerEvent event) {
348         this.txPower = (int) event.getTxPower();
349     }
350
351     @Override
352     public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
353         // Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
354         // So we need to find the characteristic by its DBUS path.
355         BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
356         if (characteristic == null) {
357             logger.debug("Received a notification for a characteristic not found on device.");
358             return;
359         }
360         BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
361         if (c != null) {
362             c.setValue(event.getData());
363             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
364         }
365     }
366
367     @Override
368     public void onRssiUpdate(RssiEvent event) {
369         int rssiTmp = event.getRssi();
370         this.rssi = rssiTmp;
371         BluetoothScanNotification notification = new BluetoothScanNotification();
372         notification.setRssi(rssiTmp);
373         notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
374     }
375
376     @Override
377     public void onConnectedStatusUpdate(ConnectedEvent event) {
378         this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
379         notifyListeners(BluetoothEventType.CONNECTION_STATE,
380                 new BluetoothConnectionStatusNotification(connectionState));
381     }
382
383     @Override
384     public boolean discoverServices() {
385         BluetoothDevice dev = device;
386         if (dev == null) {
387             return false;
388         }
389         if (dev.getGattServices().size() > getServices().size()) {
390             for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
391                 BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
392                         dBusBlueZService.isPrimary());
393                 for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
394                     BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
395                             UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
396
397                     for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
398                         BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
399                                 UUID.fromString(dBusBlueZDescriptor.getUuid()), 0);
400                         characteristic.addDescriptor(descriptor);
401                     }
402                     service.addCharacteristic(characteristic);
403                 }
404                 addService(service);
405             }
406             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
407         }
408         return true;
409     }
410
411     @Override
412     public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
413         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
414         if (c == null) {
415             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
416             return false;
417         }
418
419         scheduler.submit(() -> {
420             try {
421                 byte[] value = c.readValue(null);
422                 characteristic.setValue(value);
423                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
424                         BluetoothCompletionStatus.SUCCESS);
425             } catch (DBusException e) {
426                 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
427                         e.getMessage());
428                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
429                         BluetoothCompletionStatus.ERROR);
430             }
431         });
432         return true;
433     }
434
435     @Override
436     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
437         BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
438         if (c != null) {
439             try {
440                 c.stopNotify();
441             } catch (BluezFailedException e) {
442                 if (e.getMessage().contains("In Progress")) {
443                     // let's retry in 10 seconds
444                     scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
445                 } else {
446                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
447                 }
448             }
449             return true;
450         } else {
451             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
452             return false;
453         }
454     }
455
456     @Override
457     public boolean enableNotifications(BluetoothDescriptor descriptor) {
458         // Not sure if it is possible to implement this
459         return false;
460     }
461
462     @Override
463     public boolean disableNotifications(BluetoothDescriptor descriptor) {
464         // Not sure if it is possible to implement this
465         return false;
466     }
467 }