2 * Copyright (c) 2010-2021 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.Comparator;
18 import java.util.HashMap;
19 import java.util.List;
22 import java.util.UUID;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.locks.Condition;
25 import java.util.concurrent.locks.Lock;
26 import java.util.concurrent.locks.ReentrantLock;
27 import java.util.function.Supplier;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.bluetooth.BluetoothAdapter;
32 import org.openhab.binding.bluetooth.BluetoothAddress;
33 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
34 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
35 import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
36 import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
37 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
38 import org.openhab.binding.bluetooth.BluetoothDescriptor;
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.ThingTypeUID;
48 import org.openhab.core.thing.ThingUID;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
54 * {@link BluetoothDisocveryParticipant}s
56 * @author Connor Petty - Initial Contribution
59 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
61 private static final int DISCOVERY_TTL = 300;
63 private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
65 private final Lock serviceDiscoveryLock = new ReentrantLock();
66 private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
67 private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
68 private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
70 private final BluetoothDeviceSnapshot device;
71 private final Collection<BluetoothDiscoveryParticipant> participants;
72 private final Set<BluetoothAdapter> adapters;
74 private volatile boolean servicesDiscovered = false;
77 * Contains characteristic which reading is ongoing or null if no ongoing readings.
79 private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
81 public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
82 Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
83 this.participants = participants;
85 this.adapters = adapters;
89 public DiscoveryResult get() {
90 List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
91 sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
93 // first see if any of the participants that don't require a connection recognize this device
94 List<BluetoothDiscoveryParticipant> connectionParticipants = new ArrayList<>();
95 for (BluetoothDiscoveryParticipant participant : sortedParticipants) {
96 if (participant.requiresConnection(device)) {
97 connectionParticipants.add(participant);
101 DiscoveryResult result = participant.createResult(device);
102 if (result != null) {
105 } catch (RuntimeException e) {
106 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
110 // Since we couldn't find a result, lets try the connection based participants
111 DiscoveryResult result = null;
112 BluetoothAddress address = device.getAddress();
113 if (isAddressAvailable(address)) {
114 result = findConnectionResult(connectionParticipants);
115 // make sure to disconnect before letting go of the device
116 if (device.getConnectionState() == ConnectionState.CONNECTED) {
118 if (!device.disconnect()) {
119 logger.debug("Failed to disconnect from device {}", address);
121 } catch (RuntimeException ex) {
122 logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
123 device.getAdapter().getUID(), ex);
127 if (result == null) {
128 result = createDefaultResult();
133 private boolean isAddressAvailable(BluetoothAddress address) {
134 // if a device with this address has a handler on any of the adapters, we abandon discovery
135 return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address));
138 private DiscoveryResult createDefaultResult() {
139 // We did not find a thing type for this device, so let's treat it as a generic beacon
140 String label = device.getName();
141 if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
142 label = "Bluetooth Device";
145 Map<String, Object> properties = new HashMap<>();
146 properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
147 Integer txPower = device.getTxPower();
148 if (txPower != null && txPower > 0) {
149 properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
151 String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
152 if (manufacturer == null) {
153 logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
155 properties.put(Thing.PROPERTY_VENDOR, manufacturer);
156 label += " (" + manufacturer + ")";
159 ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
161 ThingUID thingUID = new ThingUID(thingTypeUID, device.getAdapter().getUID(),
162 device.getAddress().toString().toLowerCase().replace(":", ""));
163 // Create the discovery result and add to the inbox
164 return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
165 .withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS).withTTL(DISCOVERY_TTL)
166 .withBridge(device.getAdapter().getUID()).withLabel(label).build();
169 // this is really just a special return type for `ensureConnected`
170 private static class ConnectionException extends Exception {
174 private void ensureConnected() throws ConnectionException, InterruptedException {
175 if (device.getConnectionState() != ConnectionState.CONNECTED) {
176 if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
177 logger.debug("Connection attempt failed to start for device {}", device.getAddress());
178 // something failed, so we abandon connection discovery
179 throw new ConnectionException();
181 if (!awaitConnection(10, TimeUnit.SECONDS)) {
182 logger.debug("Connection to device {} timed out", device.getAddress());
183 throw new ConnectionException();
185 if (!servicesDiscovered) {
186 device.discoverServices();
187 if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
188 logger.debug("Service discovery for device {} timed out", device.getAddress());
189 // something failed, so we abandon connection discovery
190 throw new ConnectionException();
193 readDeviceInformationIfMissing();
194 logger.debug("Device information fetched from the device: {}", device);
198 private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
200 device.addListener(this);
201 for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
202 // we call this every time just in case a participant somehow closes the connection
205 DiscoveryResult result = participant.createResult(device);
206 if (result != null) {
209 } catch (RuntimeException e) {
210 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
213 } catch (InterruptedException | ConnectionException e) {
216 device.removeListener(this);
222 public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
226 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
227 if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
228 serviceDiscoveryLock.lock();
230 connectionCondition.signal();
232 serviceDiscoveryLock.unlock();
237 private void readDeviceInformationIfMissing() throws InterruptedException {
238 if (device.getName() == null) {
239 fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
241 if (device.getModel() == null) {
242 fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
244 if (device.getSerialNumber() == null) {
245 fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
247 if (device.getHardwareRevision() == null) {
248 fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
250 if (device.getFirmwareRevision() == null) {
251 fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
253 if (device.getSoftwareRevision() == null) {
254 fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
258 private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
259 UUID uuid = gattCharacteristic.getUUID();
260 BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
261 if (characteristic == null) {
262 logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
265 if (!device.readCharacteristic(characteristic)) {
266 logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
269 ongoingGattCharacteristic = gattCharacteristic;
270 if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
271 logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
272 ongoingGattCharacteristic = null;
276 private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
277 serviceDiscoveryLock.lock();
279 long nanosTimeout = unit.toNanos(timeout);
280 while (device.getConnectionState() != ConnectionState.CONNECTED) {
281 if (nanosTimeout <= 0L) {
284 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
287 serviceDiscoveryLock.unlock();
292 private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
293 serviceDiscoveryLock.lock();
295 long nanosTimeout = unit.toNanos(timeout);
296 while (ongoingGattCharacteristic != null) {
297 if (nanosTimeout <= 0L) {
300 nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
303 serviceDiscoveryLock.unlock();
308 private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
309 serviceDiscoveryLock.lock();
311 long nanosTimeout = unit.toNanos(timeout);
312 while (!servicesDiscovered) {
313 if (nanosTimeout <= 0L) {
316 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
319 serviceDiscoveryLock.unlock();
325 public void onServicesDiscovered() {
326 serviceDiscoveryLock.lock();
328 servicesDiscovered = true;
329 serviceDiscoveryCondition.signal();
331 serviceDiscoveryLock.unlock();
336 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
337 serviceDiscoveryLock.lock();
339 if (status == BluetoothCompletionStatus.SUCCESS) {
340 switch (characteristic.getGattCharacteristic()) {
342 device.setName(characteristic.getStringValue(0));
344 case MODEL_NUMBER_STRING:
345 device.setModel(characteristic.getStringValue(0));
347 case SERIAL_NUMBER_STRING:
348 device.setSerialNumberl(characteristic.getStringValue(0));
350 case HARDWARE_REVISION_STRING:
351 device.setHardwareRevision(characteristic.getStringValue(0));
353 case FIRMWARE_REVISION_STRING:
354 device.setFirmwareRevision(characteristic.getStringValue(0));
356 case SOFTWARE_REVISION_STRING:
357 device.setSoftwareRevision(characteristic.getStringValue(0));
364 if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
365 ongoingGattCharacteristic = null;
366 infoDiscoveryCondition.signal();
369 serviceDiscoveryLock.unlock();
374 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
375 BluetoothCompletionStatus status) {
379 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
383 public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
387 public void onAdapterChanged(BluetoothAdapter adapter) {