]> git.basschouten.com Git - openhab-addons.git/blob
0f099bde411906b6633726d9ec22da2f7d9e18c7
[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;
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.openhab.binding.bluetooth.BaseBluetoothDevice;
22 import org.openhab.binding.bluetooth.BluetoothAddress;
23 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
24 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
25 import org.openhab.binding.bluetooth.BluetoothDescriptor;
26 import org.openhab.binding.bluetooth.BluetoothService;
27 import org.openhab.binding.bluetooth.bluez.handler.BlueZBridgeHandler;
28 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
29 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
30 import org.openhab.core.common.ThreadPoolManager;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import tinyb.BluetoothException;
36 import tinyb.BluetoothGattCharacteristic;
37 import tinyb.BluetoothGattDescriptor;
38 import tinyb.BluetoothGattService;
39
40 /**
41  * Implementation of BluetoothDevice for BlueZ via TinyB
42  *
43  * @author Kai Kreuzer - Initial contribution and API
44  *
45  */
46 public class BlueZBluetoothDevice extends BaseBluetoothDevice {
47
48     private tinyb.BluetoothDevice device;
49
50     private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
51
52     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
53
54     /**
55      * Constructor
56      *
57      * @param adapter the bridge handler through which this device is connected
58      * @param address the Bluetooth address of the device
59      * @param name the name of the device
60      */
61     public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
62         super(adapter, address);
63         logger.debug("Creating BlueZ device with address '{}'", address);
64     }
65
66     /**
67      * Initializes a newly created instance of this class.
68      * This method should always be called directly after creating a new object instance.
69      */
70     public void initialize() {
71         updateLastSeenTime();
72     }
73
74     /**
75      * Updates the internally used tinyB device instance. It replaces any previous instance, disables notifications on
76      * it and enables notifications on the new instance.
77      *
78      * @param tinybDevice the new device instance to use for communication
79      */
80     public synchronized void updateTinybDevice(tinyb.BluetoothDevice tinybDevice) {
81         if (Objects.equals(device, tinybDevice)) {
82             return;
83         }
84
85         if (device != null) {
86             // we need to replace the instance - let's deactivate notifications on the old one
87             disableNotifications();
88         }
89         this.device = tinybDevice;
90
91         if (this.device == null) {
92             return;
93         }
94         updateLastSeenTime();
95
96         this.name = device.getName();
97         this.rssi = (int) device.getRSSI();
98         this.txPower = (int) device.getTxPower();
99
100         device.getManufacturerData().entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
101                 .ifPresent(manufacturerId ->
102                 // Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
103                 this.manufacturer = manufacturerId & 0xFFFF);
104
105         if (device.getConnected()) {
106             this.connectionState = ConnectionState.CONNECTED;
107         }
108
109         enableNotifications();
110         refreshServices();
111     }
112
113     private void enableNotifications() {
114         logger.debug("Enabling notifications for device '{}'", device.getAddress());
115         device.enableRSSINotifications(n -> {
116             updateLastSeenTime();
117             rssi = (int) n;
118             BluetoothScanNotification notification = new BluetoothScanNotification();
119             notification.setRssi(n);
120             notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
121         });
122         device.enableManufacturerDataNotifications(n -> {
123             updateLastSeenTime();
124             for (Map.Entry<Short, byte[]> entry : n.entrySet()) {
125                 BluetoothScanNotification notification = new BluetoothScanNotification();
126                 byte[] data = new byte[entry.getValue().length + 2];
127                 data[0] = (byte) (entry.getKey() & 0xFF);
128                 data[1] = (byte) (entry.getKey() >>> 8);
129                 System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
130                 if (logger.isDebugEnabled()) {
131                     logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
132                 }
133                 notification.setManufacturerData(data);
134                 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
135             }
136         });
137         device.enableConnectedNotifications(connected -> {
138             updateLastSeenTime();
139             connectionState = connected ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
140             logger.debug("Connection state of '{}' changed to {}", address, connectionState);
141             notifyListeners(BluetoothEventType.CONNECTION_STATE,
142                     new BluetoothConnectionStatusNotification(connectionState));
143         });
144         device.enableServicesResolvedNotifications(resolved -> {
145             updateLastSeenTime();
146             logger.debug("Received services resolved event for '{}': {}", address, resolved);
147             if (resolved) {
148                 refreshServices();
149                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
150             }
151         });
152         device.enableServiceDataNotifications(data -> {
153             updateLastSeenTime();
154             if (logger.isDebugEnabled()) {
155                 logger.debug("Received service data for '{}':", address);
156                 for (Map.Entry<String, byte[]> entry : data.entrySet()) {
157                     logger.debug("{} : {}", entry.getKey(), HexUtils.bytesToHex(entry.getValue(), " "));
158                 }
159             }
160         });
161     }
162
163     private void disableNotifications() {
164         logger.debug("Disabling notifications for device '{}'", device.getAddress());
165         device.disableBlockedNotifications();
166         device.disableManufacturerDataNotifications();
167         device.disablePairedNotifications();
168         device.disableRSSINotifications();
169         device.disableServiceDataNotifications();
170         device.disableTrustedNotifications();
171     }
172
173     protected void refreshServices() {
174         if (device.getServices().size() > getServices().size()) {
175             for (BluetoothGattService tinybService : device.getServices()) {
176                 BluetoothService service = new BluetoothService(UUID.fromString(tinybService.getUUID()),
177                         tinybService.getPrimary());
178                 for (BluetoothGattCharacteristic tinybCharacteristic : tinybService.getCharacteristics()) {
179                     BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
180                             UUID.fromString(tinybCharacteristic.getUUID()), 0);
181                     for (BluetoothGattDescriptor tinybDescriptor : tinybCharacteristic.getDescriptors()) {
182                         BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
183                                 UUID.fromString(tinybDescriptor.getUUID()));
184                         characteristic.addDescriptor(descriptor);
185                     }
186                     service.addCharacteristic(characteristic);
187                 }
188                 addService(service);
189             }
190             notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
191         }
192     }
193
194     @Override
195     public boolean connect() {
196         if (device != null && !device.getConnected()) {
197             try {
198                 return device.connect();
199             } catch (BluetoothException e) {
200                 if ("Timeout was reached".equals(e.getMessage())) {
201                     notifyListeners(BluetoothEventType.CONNECTION_STATE,
202                             new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
203                 } else if (e.getMessage() != null && e.getMessage().contains("Protocol not available")) {
204                     // this device does not seem to be connectable at all - let's log a warning and ignore it.
205                     logger.warn("Bluetooth device '{}' does not allow a connection.", device.getAddress());
206                 } else {
207                     logger.debug("Exception occurred when trying to connect device '{}': {}", device.getAddress(),
208                             e.getMessage());
209                 }
210             }
211         }
212         return false;
213     }
214
215     @Override
216     public boolean disconnect() {
217         if (device != null && device.getConnected()) {
218             logger.debug("Disconnecting '{}'", address);
219             try {
220                 return device.disconnect();
221             } catch (BluetoothException e) {
222                 logger.debug("Exception occurred when trying to disconnect device '{}': {}", device.getAddress(),
223                         e.getMessage());
224             }
225         }
226         return false;
227     }
228
229     @Override
230     public boolean discoverServices() {
231         return false;
232     }
233
234     private void ensureConnected() {
235         if (device == null || !device.getConnected()) {
236             throw new IllegalStateException("TinyB device is not set or not connected");
237         }
238     }
239
240     @Override
241     public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
242         ensureConnected();
243
244         BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
245         if (c == null) {
246             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
247             return false;
248         }
249         scheduler.submit(() -> {
250             try {
251                 byte[] value = c.readValue();
252                 characteristic.setValue(value);
253                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
254                         BluetoothCompletionStatus.SUCCESS);
255             } catch (BluetoothException e) {
256                 logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
257                         e.getMessage());
258                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
259                         BluetoothCompletionStatus.ERROR);
260             }
261         });
262         return true;
263     }
264
265     @Override
266     public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
267         ensureConnected();
268
269         BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
270         if (c == null) {
271             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
272             return false;
273         }
274         scheduler.submit(() -> {
275             try {
276                 BluetoothCompletionStatus successStatus = c.writeValue(characteristic.getByteValue())
277                         ? BluetoothCompletionStatus.SUCCESS
278                         : BluetoothCompletionStatus.ERROR;
279                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic, successStatus);
280             } catch (BluetoothException e) {
281                 logger.debug("Exception occurred when trying to write characteristic '{}': {}",
282                         characteristic.getUuid(), e.getMessage());
283                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
284                         BluetoothCompletionStatus.ERROR);
285             }
286         });
287         return true;
288     }
289
290     @Override
291     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
292         ensureConnected();
293
294         BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
295         if (c != null) {
296             try {
297                 c.enableValueNotifications(value -> {
298                     characteristic.setValue(value);
299                     notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
300                 });
301             } catch (BluetoothException e) {
302                 if (e.getMessage().contains("Already notifying")) {
303                     return false;
304                 } else if (e.getMessage().contains("In Progress")) {
305                     // let's retry in 10 seconds
306                     scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
307                 } else {
308                     logger.warn("Exception occurred while activating notifications on '{}'", address, e);
309                 }
310             }
311             return true;
312         } else {
313             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
314             return false;
315         }
316     }
317
318     @Override
319     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
320         ensureConnected();
321
322         BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
323         if (c != null) {
324             c.disableValueNotifications();
325             return true;
326         } else {
327             logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
328             return false;
329         }
330     }
331
332     @Override
333     public boolean enableNotifications(BluetoothDescriptor descriptor) {
334         ensureConnected();
335
336         BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
337         if (d != null) {
338             d.enableValueNotifications(value -> {
339                 descriptor.setValue(value);
340                 notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
341             });
342             return true;
343         } else {
344             logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
345             return false;
346         }
347     }
348
349     @Override
350     public boolean disableNotifications(BluetoothDescriptor descriptor) {
351         ensureConnected();
352
353         BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
354         if (d != null) {
355             d.disableValueNotifications();
356             return true;
357         } else {
358             logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
359             return false;
360         }
361     }
362
363     private BluetoothGattCharacteristic getTinybCharacteristicByUUID(String uuid) {
364         for (BluetoothGattService service : device.getServices()) {
365             for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
366                 if (c.getUUID().equals(uuid)) {
367                     return c;
368                 }
369             }
370         }
371         return null;
372     }
373
374     private BluetoothGattDescriptor getTinybDescriptorByUUID(String uuid) {
375         for (BluetoothGattService service : device.getServices()) {
376             for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
377                 for (BluetoothGattDescriptor d : c.getDescriptors()) {
378                     if (d.getUUID().equals(uuid)) {
379                         return d;
380                     }
381                 }
382             }
383         }
384         return null;
385     }
386
387     /**
388      * Clean up and release memory.
389      */
390     @Override
391     public void dispose() {
392         if (device == null) {
393             return;
394         }
395         disableNotifications();
396         try {
397             device.remove();
398         } catch (BluetoothException ex) {
399             if (ex.getMessage().contains("Does Not Exist")) {
400                 // this happens when the underlying device has already been removed
401                 // but we don't have a way to check if that is the case beforehand so
402                 // we will just eat the error here.
403             } else {
404                 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
405                         ex.getMessage());
406             }
407         }
408     }
409 }