]> git.basschouten.com Git - openhab-addons.git/blob
d048051ed67f06b4c4b8c159dc618978c773410d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.discovery.internal;
14
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;
20 import java.util.Map;
21 import java.util.Set;
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;
28
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;
47
48 /**
49  * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
50  * {@link BluetoothDisocveryParticipant}s
51  *
52  * @author Connor Petty - Initial Contribution
53  */
54 @NonNullByDefault
55 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult> {
56
57     private static final int DISCOVERY_TTL = 300;
58
59     private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
60
61     private final BluetoothDeviceSnapshot device;
62     private final Collection<BluetoothDiscoveryParticipant> participants;
63     private final Set<BluetoothAdapter> adapters;
64
65     public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
66             Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
67         this.participants = participants;
68         this.device = device;
69         this.adapters = adapters;
70     }
71
72     @Override
73     public DiscoveryResult get() {
74         List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
75         sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
76
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);
82                 continue;
83             }
84             try {
85                 DiscoveryResult result = participant.createResult(device);
86                 if (result != null) {
87                     return result;
88                 }
89             } catch (RuntimeException e) {
90                 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
91             }
92         }
93
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) {
101                 try {
102                     if (!device.disconnect()) {
103                         logger.debug("Failed to disconnect from device {}", address);
104                     }
105                 } catch (RuntimeException ex) {
106                     logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
107                             device.getAdapter().getUID(), ex);
108                 }
109             }
110         }
111         if (result == null) {
112             result = createDefaultResult();
113         }
114         return result;
115     }
116
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));
120     }
121
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";
127         }
128
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));
134         }
135         String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
136         if (manufacturer == null) {
137             logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
138         } else {
139             properties.put(Thing.PROPERTY_VENDOR, manufacturer);
140             label += " (" + manufacturer + ")";
141         }
142
143         ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
144
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();
151     }
152
153     private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
154         try {
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
160                         return null;
161                     }
162                     if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
163                         logger.debug("Connection to device {} timed out", device.getAddress());
164                         return null;
165                     }
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
171                             return null;
172                         }
173                     }
174                     readDeviceInformationIfMissing();
175                     logger.debug("Device information fetched from the device: {}", device);
176                 }
177
178                 try {
179                     DiscoveryResult result = participant.createResult(device);
180                     if (result != null) {
181                         return result;
182                     }
183                 } catch (RuntimeException e) {
184                     logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
185                 }
186             }
187         } catch (InterruptedException e) {
188             // do nothing
189         }
190         return null;
191     }
192
193     private void readDeviceInformationIfMissing() throws InterruptedException {
194         if (device.getName() == null) {
195             fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME, device::setName);
196         }
197         if (device.getModel() == null) {
198             fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING, device::setModel);
199         }
200         if (device.getSerialNumber() == null) {
201             fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING, device::setSerialNumberl);
202         }
203         if (device.getHardwareRevision() == null) {
204             fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING, device::setHardwareRevision);
205         }
206         if (device.getFirmwareRevision() == null) {
207             fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING, device::setFirmwareRevision);
208         }
209         if (device.getSoftwareRevision() == null) {
210             fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING, device::setSoftwareRevision);
211         }
212     }
213
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);
220             return;
221         }
222         try {
223             byte[] value = device.readCharacteristic(characteristic).get(1, TimeUnit.SECONDS);
224             consumer.accept(BluetoothUtils.getStringValue(value, 0));
225         } catch (ExecutionException e) {
226             logger.debug("Failed to aquire uuid {} from device {}: {}", uuid, device.getAddress(), e.getMessage());
227         } catch (TimeoutException e) {
228             logger.debug("Device info (uuid {}) for device {} timed out: {}", uuid, device.getAddress(),
229                     e.getMessage());
230         }
231     }
232 }