2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.bluez;
16 import java.util.Objects;
17 import java.util.UUID;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.TimeUnit;
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;
35 import tinyb.BluetoothException;
36 import tinyb.BluetoothGattCharacteristic;
37 import tinyb.BluetoothGattDescriptor;
38 import tinyb.BluetoothGattService;
41 * Implementation of BluetoothDevice for BlueZ via TinyB
43 * @author Kai Kreuzer - Initial contribution and API
46 public class BlueZBluetoothDevice extends BaseBluetoothDevice {
48 private tinyb.BluetoothDevice device;
50 private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
52 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
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
61 public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
62 super(adapter, address);
63 logger.debug("Creating BlueZ device with address '{}'", address);
67 * Initializes a newly created instance of this class.
68 * This method should always be called directly after creating a new object instance.
70 public void initialize() {
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.
78 * @param tinybDevice the new device instance to use for communication
80 public synchronized void updateTinybDevice(tinyb.BluetoothDevice tinybDevice) {
81 if (Objects.equals(device, tinybDevice)) {
86 // we need to replace the instance - let's deactivate notifications on the old one
87 disableNotifications();
89 this.device = tinybDevice;
91 if (this.device == null) {
96 this.name = device.getName();
97 this.rssi = (int) device.getRSSI();
98 this.txPower = (int) device.getTxPower();
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);
105 if (device.getConnected()) {
106 this.connectionState = ConnectionState.CONNECTED;
109 enableNotifications();
113 private void enableNotifications() {
114 logger.debug("Enabling notifications for device '{}'", device.getAddress());
115 device.enableRSSINotifications(n -> {
116 updateLastSeenTime();
118 BluetoothScanNotification notification = new BluetoothScanNotification();
119 notification.setRssi(n);
120 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
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, " "));
133 notification.setManufacturerData(data);
134 notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
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));
144 device.enableServicesResolvedNotifications(resolved -> {
145 updateLastSeenTime();
146 logger.debug("Received services resolved event for '{}': {}", address, resolved);
149 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
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(), " "));
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();
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);
186 service.addCharacteristic(characteristic);
190 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
195 public boolean connect() {
196 if (device != null && !device.getConnected()) {
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());
207 logger.debug("Exception occurred when trying to connect device '{}': {}", device.getAddress(),
216 public boolean disconnect() {
217 if (device != null && device.getConnected()) {
218 logger.debug("Disconnecting '{}'", address);
220 return device.disconnect();
221 } catch (BluetoothException e) {
222 logger.debug("Exception occurred when trying to disconnect device '{}': {}", device.getAddress(),
230 public boolean discoverServices() {
234 private void ensureConnected() {
235 if (device == null || !device.getConnected()) {
236 throw new IllegalStateException("TinyB device is not set or not connected");
241 public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
244 BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
246 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
249 scheduler.submit(() -> {
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(),
258 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
259 BluetoothCompletionStatus.ERROR);
266 public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
269 BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
271 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
274 scheduler.submit(() -> {
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);
291 public boolean enableNotifications(BluetoothCharacteristic characteristic) {
294 BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
297 c.enableValueNotifications(value -> {
298 characteristic.setValue(value);
299 notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
301 } catch (BluetoothException e) {
302 if (e.getMessage().contains("Already notifying")) {
304 } else if (e.getMessage().contains("In Progress")) {
305 // let's retry in 10 seconds
306 scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
308 logger.warn("Exception occurred while activating notifications on '{}'", address, e);
313 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
319 public boolean disableNotifications(BluetoothCharacteristic characteristic) {
322 BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
324 c.disableValueNotifications();
327 logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
333 public boolean enableNotifications(BluetoothDescriptor descriptor) {
336 BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
338 d.enableValueNotifications(value -> {
339 descriptor.setValue(value);
340 notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
344 logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
350 public boolean disableNotifications(BluetoothDescriptor descriptor) {
353 BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
355 d.disableValueNotifications();
358 logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
363 private BluetoothGattCharacteristic getTinybCharacteristicByUUID(String uuid) {
364 for (BluetoothGattService service : device.getServices()) {
365 for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
366 if (c.getUUID().equals(uuid)) {
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)) {
388 * Clean up and release memory.
391 public void dispose() {
392 if (device == null) {
395 disableNotifications();
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.
404 logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,