2 * Copyright (c) 2010-2023 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.ExecutionException;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26 import java.util.function.Consumer;
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.BluetoothDevice.ConnectionState;
38 import org.openhab.binding.bluetooth.BluetoothUtils;
39 import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
40 import org.openhab.core.config.discovery.DiscoveryResult;
41 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingTypeUID;
44 import org.openhab.core.thing.ThingUID;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
50 * {@link BluetoothDisocveryParticipant}s
52 * @author Connor Petty - Initial Contribution
55 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult> {
57 private static final int DISCOVERY_TTL = 300;
59 private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
61 private final BluetoothDeviceSnapshot device;
62 private final Collection<BluetoothDiscoveryParticipant> participants;
63 private final Set<BluetoothAdapter> adapters;
65 public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
66 Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
67 this.participants = participants;
69 this.adapters = adapters;
73 public DiscoveryResult get() {
74 List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
75 sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
77 // first see if any of the participants that don't require a connection recognize this device
78 List<BluetoothDiscoveryParticipant> connectionParticipants = new ArrayList<>();
79 for (BluetoothDiscoveryParticipant participant : sortedParticipants) {
80 if (participant.requiresConnection(device)) {
81 connectionParticipants.add(participant);
85 DiscoveryResult result = participant.createResult(device);
89 } catch (RuntimeException e) {
90 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
94 // Since we couldn't find a result, lets try the connection based participants
95 DiscoveryResult result = null;
96 BluetoothAddress address = device.getAddress();
97 if (isAddressAvailable(address)) {
98 result = findConnectionResult(connectionParticipants);
99 // make sure to disconnect before letting go of the device
100 if (device.getConnectionState() == ConnectionState.CONNECTED) {
102 if (!device.disconnect()) {
103 logger.debug("Failed to disconnect from device {}", address);
105 } catch (RuntimeException ex) {
106 logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
107 device.getAdapter().getUID(), ex);
111 if (result == null) {
112 result = createDefaultResult();
117 private boolean isAddressAvailable(BluetoothAddress address) {
118 // if a device with this address has a handler on any of the adapters, we abandon discovery
119 return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address));
122 private DiscoveryResult createDefaultResult() {
123 // We did not find a thing type for this device, so let's treat it as a generic beacon
124 String label = device.getName();
125 if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
126 label = "Bluetooth Device";
129 Map<String, Object> properties = new HashMap<>();
130 properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
131 Integer txPower = device.getTxPower();
132 if (txPower != null && txPower > 0) {
133 properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
135 String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
136 if (manufacturer == null) {
137 logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
139 properties.put(Thing.PROPERTY_VENDOR, manufacturer);
140 label += " (" + manufacturer + ")";
143 ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
145 ThingUID thingUID = new ThingUID(thingTypeUID, device.getAdapter().getUID(),
146 device.getAddress().toString().toLowerCase().replace(":", ""));
147 // Create the discovery result and add to the inbox
148 return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
149 .withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS).withTTL(DISCOVERY_TTL)
150 .withBridge(device.getAdapter().getUID()).withLabel(label).build();
153 private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
155 for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
156 if (device.getConnectionState() != ConnectionState.CONNECTED) {
157 if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
158 logger.debug("Connection attempt failed to start for device {}", device.getAddress());
159 // something failed, so we abandon connection discovery
162 if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
163 logger.debug("Connection to device {} timed out", device.getAddress());
166 if (!device.isServicesDiscovered()) {
167 device.discoverServices();
168 if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
169 logger.debug("Service discovery for device {} timed out", device.getAddress());
170 // something failed, so we abandon connection discovery
174 readDeviceInformationIfMissing();
175 logger.debug("Device information fetched from the device: {}", device);
179 DiscoveryResult result = participant.createResult(device);
180 if (result != null) {
183 } catch (RuntimeException e) {
184 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
187 } catch (InterruptedException e) {
193 private void readDeviceInformationIfMissing() throws InterruptedException {
194 if (device.getName() == null) {
195 fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName);
197 if (device.getModel() == null) {
198 fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel);
200 if (device.getSerialNumber() == null) {
201 fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl);
203 if (device.getHardwareRevision() == null) {
204 fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision);
206 if (device.getFirmwareRevision() == null) {
207 fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision);
209 if (device.getSoftwareRevision() == null) {
210 fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision);
214 private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic, Consumer<String> consumer)
215 throws InterruptedException {
216 UUID uuid = gattCharacteristic.getUUID();
217 BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
218 if (characteristic == null) {
219 logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
223 byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS);
224 String strValue = BluetoothUtils.getStringValue(value, 0);
225 if (strValue != null) {
226 consumer.accept(strValue);
228 } catch (ExecutionException e) {
229 logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage());
230 } catch (TimeoutException e) {
231 logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(),