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.discovery.internal;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.HashMap;
18 import java.util.List;
21 import java.util.UUID;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.locks.Condition;
24 import java.util.concurrent.locks.Lock;
25 import java.util.concurrent.locks.ReentrantLock;
26 import java.util.function.Supplier;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.bluetooth.BluetoothAdapter;
31 import org.openhab.binding.bluetooth.BluetoothAddress;
32 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
33 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
34 import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
35 import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
36 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
37 import org.openhab.binding.bluetooth.BluetoothDescriptor;
38 import org.openhab.binding.bluetooth.BluetoothDevice;
39 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
40 import org.openhab.binding.bluetooth.BluetoothDeviceListener;
41 import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
42 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
43 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
44 import org.openhab.core.config.discovery.DiscoveryResult;
45 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingUID;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
53 * {@link BluetoothDisocveryParticipant}s
55 * @author Connor Petty - Initial Contribution
58 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
60 private static final int DISCOVERY_TTL = 300;
62 private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
64 private final Lock serviceDiscoveryLock = new ReentrantLock();
65 private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
66 private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
67 private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
69 private final BluetoothDeviceSnapshot device;
70 private final Collection<BluetoothDiscoveryParticipant> participants;
71 private final Set<BluetoothAdapter> adapters;
73 private volatile boolean servicesDiscovered = false;
76 * Contains characteristic which reading is ongoing or null if no ongoing readings.
78 private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
80 public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
81 Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
82 this.participants = participants;
84 this.adapters = adapters;
88 public DiscoveryResult get() {
89 // first see if any of the participants that don't require a connection recognize this device
90 List<BluetoothDiscoveryParticipant> connectionParticipants = new ArrayList<>();
91 for (BluetoothDiscoveryParticipant participant : participants) {
92 if (participant.requiresConnection(device)) {
93 connectionParticipants.add(participant);
97 DiscoveryResult result = participant.createResult(device);
101 } catch (RuntimeException e) {
102 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
106 // Since we couldn't find a result, lets try the connection based participants
107 DiscoveryResult result = null;
108 if (!connectionParticipants.isEmpty()) {
109 BluetoothAddress address = device.getAddress();
110 if (isAddressAvailable(address)) {
111 result = findConnectionResult(connectionParticipants);
112 // make sure to disconnect before letting go of the device
113 if (device.getConnectionState() == ConnectionState.CONNECTED) {
115 if (!device.disconnect()) {
116 logger.debug("Failed to disconnect from device {}", address);
118 } catch (RuntimeException ex) {
119 logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
120 device.getAdapter().getUID(), ex);
125 if (result == null) {
126 result = createDefaultResult(device);
131 private boolean isAddressAvailable(BluetoothAddress address) {
132 // if a device with this address has a handler on any of the adapters, we abandon discovery
133 return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address));
136 private DiscoveryResult createDefaultResult(BluetoothDevice device) {
137 // We did not find a thing type for this device, so let's treat it as a generic one
138 String label = device.getName();
139 if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
140 label = "Bluetooth Device";
143 Map<String, Object> properties = new HashMap<>();
144 properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
145 Integer txPower = device.getTxPower();
146 if (txPower != null && txPower > 0) {
147 properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
149 String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
150 if (manufacturer == null) {
151 logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
153 properties.put(Thing.PROPERTY_VENDOR, manufacturer);
154 label += " (" + manufacturer + ")";
157 ThingUID thingUID = new ThingUID(BluetoothBindingConstants.THING_TYPE_BEACON, device.getAdapter().getUID(),
158 device.getAddress().toString().toLowerCase().replace(":", ""));
160 // Create the discovery result and add to the inbox
161 return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
162 .withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS).withTTL(DISCOVERY_TTL)
163 .withBridge(device.getAdapter().getUID()).withLabel(label).build();
166 private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
168 device.addListener(this);
169 for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
170 // we call this every time just in case a participant somehow closes the connection
171 if (device.getConnectionState() != ConnectionState.CONNECTED) {
172 if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
173 logger.debug("Connection attempt failed to start for device {}", device.getAddress());
174 // something failed, so we abandon connection discovery
177 if (!awaitConnection(1, TimeUnit.SECONDS)) {
178 logger.debug("Connection to device {} timed out", device.getAddress());
181 if (!servicesDiscovered) {
182 device.discoverServices();
183 if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
184 logger.debug("Service discovery for device {} timed out", device.getAddress());
185 // something failed, so we abandon connection discovery
189 readDeviceInformationIfMissing();
190 logger.debug("Device information fetched from the device: {}", device);
194 DiscoveryResult result = participant.createResult(device);
195 if (result != null) {
198 } catch (RuntimeException e) {
199 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
202 } catch (InterruptedException e) {
205 device.removeListener(this);
211 public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
215 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
216 if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
217 serviceDiscoveryLock.lock();
219 connectionCondition.signal();
221 serviceDiscoveryLock.unlock();
226 private void readDeviceInformationIfMissing() throws InterruptedException {
227 if (device.getName() == null) {
228 fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
230 if (device.getModel() == null) {
231 fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
233 if (device.getSerialNumber() == null) {
234 fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
236 if (device.getHardwareRevision() == null) {
237 fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
239 if (device.getFirmwareRevision() == null) {
240 fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
242 if (device.getSoftwareRevision() == null) {
243 fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
247 private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
248 UUID uuid = gattCharacteristic.getUUID();
249 BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
250 if (characteristic == null) {
251 logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
254 if (!device.readCharacteristic(characteristic)) {
255 logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
258 ongoingGattCharacteristic = gattCharacteristic;
259 if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
260 logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
261 ongoingGattCharacteristic = null;
265 private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
266 serviceDiscoveryLock.lock();
268 long nanosTimeout = unit.toNanos(timeout);
269 while (device.getConnectionState() != ConnectionState.CONNECTED) {
270 if (nanosTimeout <= 0L) {
273 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
276 serviceDiscoveryLock.unlock();
281 private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
282 serviceDiscoveryLock.lock();
284 long nanosTimeout = unit.toNanos(timeout);
285 while (ongoingGattCharacteristic != null) {
286 if (nanosTimeout <= 0L) {
289 nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
292 serviceDiscoveryLock.unlock();
297 private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
298 serviceDiscoveryLock.lock();
300 long nanosTimeout = unit.toNanos(timeout);
301 while (!servicesDiscovered) {
302 if (nanosTimeout <= 0L) {
305 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
308 serviceDiscoveryLock.unlock();
314 public void onServicesDiscovered() {
315 serviceDiscoveryLock.lock();
317 servicesDiscovered = true;
318 serviceDiscoveryCondition.signal();
320 serviceDiscoveryLock.unlock();
325 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
326 serviceDiscoveryLock.lock();
328 if (status == BluetoothCompletionStatus.SUCCESS) {
329 switch (characteristic.getGattCharacteristic()) {
331 device.setName(characteristic.getStringValue(0));
333 case MODEL_NUMBER_STRING:
334 device.setModel(characteristic.getStringValue(0));
336 case SERIAL_NUMBER_STRING:
337 device.setSerialNumberl(characteristic.getStringValue(0));
339 case HARDWARE_REVISION_STRING:
340 device.setHardwareRevision(characteristic.getStringValue(0));
342 case FIRMWARE_REVISION_STRING:
343 device.setFirmwareRevision(characteristic.getStringValue(0));
345 case SOFTWARE_REVISION_STRING:
346 device.setSoftwareRevision(characteristic.getStringValue(0));
353 if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
354 ongoingGattCharacteristic = null;
355 infoDiscoveryCondition.signal();
358 serviceDiscoveryLock.unlock();
363 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
364 BluetoothCompletionStatus status) {
368 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
372 public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
376 public void onAdapterChanged(BluetoothAdapter adapter) {