]> git.basschouten.com Git - openhab-addons.git/blob
9924a5012ac3bf60832a12d6717ff782de08d3cf
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Set;
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;
27
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;
50
51 /**
52  * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
53  * {@link BluetoothDisocveryParticipant}s
54  *
55  * @author Connor Petty - Initial Contribution
56  */
57 @NonNullByDefault
58 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
59
60     private static final int DISCOVERY_TTL = 300;
61
62     private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
63
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();
68
69     private final BluetoothDeviceSnapshot device;
70     private final Collection<BluetoothDiscoveryParticipant> participants;
71     private final Set<BluetoothAdapter> adapters;
72
73     private volatile boolean servicesDiscovered = false;
74
75     /**
76      * Contains characteristic which reading is ongoing or null if no ongoing readings.
77      */
78     private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
79
80     public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
81             Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
82         this.participants = participants;
83         this.device = device;
84         this.adapters = adapters;
85     }
86
87     @Override
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);
94                 continue;
95             }
96             try {
97                 DiscoveryResult result = participant.createResult(device);
98                 if (result != null) {
99                     return result;
100                 }
101             } catch (RuntimeException e) {
102                 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
103             }
104         }
105
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) {
114                     try {
115                         if (!device.disconnect()) {
116                             logger.debug("Failed to disconnect from device {}", address);
117                         }
118                     } catch (RuntimeException ex) {
119                         logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
120                                 device.getAdapter().getUID(), ex);
121                     }
122                 }
123             }
124         }
125         if (result == null) {
126             result = createDefaultResult(device);
127         }
128         return result;
129     }
130
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));
134     }
135
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";
141         }
142
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));
148         }
149         String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
150         if (manufacturer == null) {
151             logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
152         } else {
153             properties.put(Thing.PROPERTY_VENDOR, manufacturer);
154             label += " (" + manufacturer + ")";
155         }
156
157         ThingUID thingUID = new ThingUID(BluetoothBindingConstants.THING_TYPE_BEACON, device.getAdapter().getUID(),
158                 device.getAddress().toString().toLowerCase().replace(":", ""));
159
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();
164     }
165
166     private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
167         try {
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
175                         return null;
176                     }
177                     if (!awaitConnection(1, TimeUnit.SECONDS)) {
178                         logger.debug("Connection to device {} timed out", device.getAddress());
179                         return null;
180                     }
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
186                             return null;
187                         }
188                     }
189                     readDeviceInformationIfMissing();
190                     logger.debug("Device information fetched from the device: {}", device);
191                 }
192
193                 try {
194                     DiscoveryResult result = participant.createResult(device);
195                     if (result != null) {
196                         return result;
197                     }
198                 } catch (RuntimeException e) {
199                     logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
200                 }
201             }
202         } catch (InterruptedException e) {
203             // do nothing
204         } finally {
205             device.removeListener(this);
206         }
207         return null;
208     }
209
210     @Override
211     public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
212     }
213
214     @Override
215     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
216         if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
217             serviceDiscoveryLock.lock();
218             try {
219                 connectionCondition.signal();
220             } finally {
221                 serviceDiscoveryLock.unlock();
222             }
223         }
224     }
225
226     private void readDeviceInformationIfMissing() throws InterruptedException {
227         if (device.getName() == null) {
228             fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
229         }
230         if (device.getModel() == null) {
231             fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
232         }
233         if (device.getSerialNumber() == null) {
234             fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
235         }
236         if (device.getHardwareRevision() == null) {
237             fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
238         }
239         if (device.getFirmwareRevision() == null) {
240             fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
241         }
242         if (device.getSoftwareRevision() == null) {
243             fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
244         }
245     }
246
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);
252             return;
253         }
254         if (!device.readCharacteristic(characteristic)) {
255             logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
256             return;
257         }
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;
262         }
263     }
264
265     private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
266         serviceDiscoveryLock.lock();
267         try {
268             long nanosTimeout = unit.toNanos(timeout);
269             while (device.getConnectionState() != ConnectionState.CONNECTED) {
270                 if (nanosTimeout <= 0L) {
271                     return false;
272                 }
273                 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
274             }
275         } finally {
276             serviceDiscoveryLock.unlock();
277         }
278         return true;
279     }
280
281     private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
282         serviceDiscoveryLock.lock();
283         try {
284             long nanosTimeout = unit.toNanos(timeout);
285             while (ongoingGattCharacteristic != null) {
286                 if (nanosTimeout <= 0L) {
287                     return false;
288                 }
289                 nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
290             }
291         } finally {
292             serviceDiscoveryLock.unlock();
293         }
294         return true;
295     }
296
297     private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
298         serviceDiscoveryLock.lock();
299         try {
300             long nanosTimeout = unit.toNanos(timeout);
301             while (!servicesDiscovered) {
302                 if (nanosTimeout <= 0L) {
303                     return false;
304                 }
305                 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
306             }
307         } finally {
308             serviceDiscoveryLock.unlock();
309         }
310         return true;
311     }
312
313     @Override
314     public void onServicesDiscovered() {
315         serviceDiscoveryLock.lock();
316         try {
317             servicesDiscovered = true;
318             serviceDiscoveryCondition.signal();
319         } finally {
320             serviceDiscoveryLock.unlock();
321         }
322     }
323
324     @Override
325     public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
326         serviceDiscoveryLock.lock();
327         try {
328             if (status == BluetoothCompletionStatus.SUCCESS) {
329                 switch (characteristic.getGattCharacteristic()) {
330                     case DEVICE_NAME:
331                         device.setName(characteristic.getStringValue(0));
332                         break;
333                     case MODEL_NUMBER_STRING:
334                         device.setModel(characteristic.getStringValue(0));
335                         break;
336                     case SERIAL_NUMBER_STRING:
337                         device.setSerialNumberl(characteristic.getStringValue(0));
338                         break;
339                     case HARDWARE_REVISION_STRING:
340                         device.setHardwareRevision(characteristic.getStringValue(0));
341                         break;
342                     case FIRMWARE_REVISION_STRING:
343                         device.setFirmwareRevision(characteristic.getStringValue(0));
344                         break;
345                     case SOFTWARE_REVISION_STRING:
346                         device.setSoftwareRevision(characteristic.getStringValue(0));
347                         break;
348                     default:
349                         break;
350                 }
351             }
352
353             if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
354                 ongoingGattCharacteristic = null;
355                 infoDiscoveryCondition.signal();
356             }
357         } finally {
358             serviceDiscoveryLock.unlock();
359         }
360     }
361
362     @Override
363     public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
364             BluetoothCompletionStatus status) {
365     }
366
367     @Override
368     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
369     }
370
371     @Override
372     public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
373     }
374
375     @Override
376     public void onAdapterChanged(BluetoothAdapter adapter) {
377     }
378 }