]> git.basschouten.com Git - openhab-addons.git/blob
a5a55bb488347d88d4fda3bd125992310a663a38
[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.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.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;
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.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;
51
52 /**
53  * The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
54  * {@link BluetoothDisocveryParticipant}s
55  *
56  * @author Connor Petty - Initial Contribution
57  */
58 @NonNullByDefault
59 public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
60
61     private static final int DISCOVERY_TTL = 300;
62
63     private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
64
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();
69
70     private final BluetoothDeviceSnapshot device;
71     private final Collection<BluetoothDiscoveryParticipant> participants;
72     private final Set<BluetoothAdapter> adapters;
73
74     private volatile boolean servicesDiscovered = false;
75
76     /**
77      * Contains characteristic which reading is ongoing or null if no ongoing readings.
78      */
79     private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
80
81     public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
82             Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
83         this.participants = participants;
84         this.device = device;
85         this.adapters = adapters;
86     }
87
88     @Override
89     public DiscoveryResult get() {
90         List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
91         sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
92
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);
98                 continue;
99             }
100             try {
101                 DiscoveryResult result = participant.createResult(device);
102                 if (result != null) {
103                     return result;
104                 }
105             } catch (RuntimeException e) {
106                 logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
107             }
108         }
109
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) {
117                 try {
118                     if (!device.disconnect()) {
119                         logger.debug("Failed to disconnect from device {}", address);
120                     }
121                 } catch (RuntimeException ex) {
122                     logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
123                             device.getAdapter().getUID(), ex);
124                 }
125             }
126         }
127         if (result == null) {
128             result = createDefaultResult();
129         }
130         return result;
131     }
132
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));
136     }
137
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";
143         }
144
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));
150         }
151         String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
152         if (manufacturer == null) {
153             logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
154         } else {
155             properties.put(Thing.PROPERTY_VENDOR, manufacturer);
156             label += " (" + manufacturer + ")";
157         }
158
159         ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
160
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();
167     }
168
169     // this is really just a special return type for `ensureConnected`
170     private static class ConnectionException extends Exception {
171
172     }
173
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();
180             }
181             if (!awaitConnection(10, TimeUnit.SECONDS)) {
182                 logger.debug("Connection to device {} timed out", device.getAddress());
183                 throw new ConnectionException();
184             }
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();
191                 }
192             }
193             readDeviceInformationIfMissing();
194             logger.debug("Device information fetched from the device: {}", device);
195         }
196     }
197
198     private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
199         try {
200             device.addListener(this);
201             for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
202                 // we call this every time just in case a participant somehow closes the connection
203                 ensureConnected();
204                 try {
205                     DiscoveryResult result = participant.createResult(device);
206                     if (result != null) {
207                         return result;
208                     }
209                 } catch (RuntimeException e) {
210                     logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
211                 }
212             }
213         } catch (InterruptedException | ConnectionException e) {
214             // do nothing
215         } finally {
216             device.removeListener(this);
217         }
218         return null;
219     }
220
221     @Override
222     public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
223     }
224
225     @Override
226     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
227         if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
228             serviceDiscoveryLock.lock();
229             try {
230                 connectionCondition.signal();
231             } finally {
232                 serviceDiscoveryLock.unlock();
233             }
234         }
235     }
236
237     private void readDeviceInformationIfMissing() throws InterruptedException {
238         if (device.getName() == null) {
239             fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
240         }
241         if (device.getModel() == null) {
242             fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
243         }
244         if (device.getSerialNumber() == null) {
245             fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
246         }
247         if (device.getHardwareRevision() == null) {
248             fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
249         }
250         if (device.getFirmwareRevision() == null) {
251             fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
252         }
253         if (device.getSoftwareRevision() == null) {
254             fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
255         }
256     }
257
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);
263             return;
264         }
265         if (!device.readCharacteristic(characteristic)) {
266             logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
267             return;
268         }
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;
273         }
274     }
275
276     private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
277         serviceDiscoveryLock.lock();
278         try {
279             long nanosTimeout = unit.toNanos(timeout);
280             while (device.getConnectionState() != ConnectionState.CONNECTED) {
281                 if (nanosTimeout <= 0L) {
282                     return false;
283                 }
284                 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
285             }
286         } finally {
287             serviceDiscoveryLock.unlock();
288         }
289         return true;
290     }
291
292     private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
293         serviceDiscoveryLock.lock();
294         try {
295             long nanosTimeout = unit.toNanos(timeout);
296             while (ongoingGattCharacteristic != null) {
297                 if (nanosTimeout <= 0L) {
298                     return false;
299                 }
300                 nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
301             }
302         } finally {
303             serviceDiscoveryLock.unlock();
304         }
305         return true;
306     }
307
308     private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
309         serviceDiscoveryLock.lock();
310         try {
311             long nanosTimeout = unit.toNanos(timeout);
312             while (!servicesDiscovered) {
313                 if (nanosTimeout <= 0L) {
314                     return false;
315                 }
316                 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
317             }
318         } finally {
319             serviceDiscoveryLock.unlock();
320         }
321         return true;
322     }
323
324     @Override
325     public void onServicesDiscovered() {
326         serviceDiscoveryLock.lock();
327         try {
328             servicesDiscovered = true;
329             serviceDiscoveryCondition.signal();
330         } finally {
331             serviceDiscoveryLock.unlock();
332         }
333     }
334
335     @Override
336     public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
337         serviceDiscoveryLock.lock();
338         try {
339             if (status == BluetoothCompletionStatus.SUCCESS) {
340                 switch (characteristic.getGattCharacteristic()) {
341                     case DEVICE_NAME:
342                         device.setName(characteristic.getStringValue(0));
343                         break;
344                     case MODEL_NUMBER_STRING:
345                         device.setModel(characteristic.getStringValue(0));
346                         break;
347                     case SERIAL_NUMBER_STRING:
348                         device.setSerialNumberl(characteristic.getStringValue(0));
349                         break;
350                     case HARDWARE_REVISION_STRING:
351                         device.setHardwareRevision(characteristic.getStringValue(0));
352                         break;
353                     case FIRMWARE_REVISION_STRING:
354                         device.setFirmwareRevision(characteristic.getStringValue(0));
355                         break;
356                     case SOFTWARE_REVISION_STRING:
357                         device.setSoftwareRevision(characteristic.getStringValue(0));
358                         break;
359                     default:
360                         break;
361                 }
362             }
363
364             if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
365                 ongoingGattCharacteristic = null;
366                 infoDiscoveryCondition.signal();
367             }
368         } finally {
369             serviceDiscoveryLock.unlock();
370         }
371     }
372
373     @Override
374     public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
375             BluetoothCompletionStatus status) {
376     }
377
378     @Override
379     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
380     }
381
382     @Override
383     public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
384     }
385
386     @Override
387     public void onAdapterChanged(BluetoothAdapter adapter) {
388     }
389 }