]> git.basschouten.com Git - openhab-addons.git/blob
2a3c924ee98e7f06347f859188357c49a861cc1d
[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.HashMap;
16 import java.util.HashSet;
17 import java.util.Map;
18 import java.util.Optional;
19 import java.util.Set;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.CopyOnWriteArraySet;
23 import java.util.function.BiConsumer;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.bluetooth.BluetoothAdapter;
28 import org.openhab.binding.bluetooth.BluetoothAddress;
29 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
30 import org.openhab.binding.bluetooth.BluetoothDevice;
31 import org.openhab.binding.bluetooth.BluetoothDiscoveryListener;
32 import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
33 import org.openhab.core.config.discovery.AbstractDiscoveryService;
34 import org.openhab.core.config.discovery.DiscoveryResult;
35 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
36 import org.openhab.core.config.discovery.DiscoveryService;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.ThingUID;
39 import org.osgi.service.component.annotations.Activate;
40 import org.osgi.service.component.annotations.Component;
41 import org.osgi.service.component.annotations.Deactivate;
42 import org.osgi.service.component.annotations.Modified;
43 import org.osgi.service.component.annotations.Reference;
44 import org.osgi.service.component.annotations.ReferenceCardinality;
45 import org.osgi.service.component.annotations.ReferencePolicy;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 /**
50  * The {@link BluetoothDiscoveryService} handles searching for BLE devices.
51  *
52  * @author Chris Jackson - Initial Contribution
53  * @author Kai Kreuzer - Introduced BluetoothAdapters and BluetoothDiscoveryParticipants
54  * @author Connor Petty - Introduced connection based discovery and added roaming support
55  */
56 @NonNullByDefault
57 @Component(service = DiscoveryService.class, configurationPid = "discovery.bluetooth")
58 public class BluetoothDiscoveryService extends AbstractDiscoveryService implements BluetoothDiscoveryListener {
59
60     private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryService.class);
61
62     private static final int SEARCH_TIME = 15;
63
64     private final Set<BluetoothAdapter> adapters = new CopyOnWriteArraySet<>();
65     private final Set<BluetoothDiscoveryParticipant> participants = new CopyOnWriteArraySet<>();
66     @NonNullByDefault({})
67     private final Map<BluetoothAddress, DiscoveryCache> discoveryCaches = new ConcurrentHashMap<>();
68
69     private final Set<ThingTypeUID> supportedThingTypes = new CopyOnWriteArraySet<>();
70
71     public BluetoothDiscoveryService() {
72         super(SEARCH_TIME);
73         supportedThingTypes.add(BluetoothBindingConstants.THING_TYPE_BEACON);
74     }
75
76     @Override
77     @Activate
78     protected void activate(@Nullable Map<String, Object> configProperties) {
79         logger.debug("Activating Bluetooth discovery service");
80         super.activate(configProperties);
81     }
82
83     @Override
84     @Modified
85     protected void modified(@Nullable Map<String, Object> configProperties) {
86         super.modified(configProperties);
87     }
88
89     @Override
90     @Deactivate
91     public void deactivate() {
92         logger.debug("Deactivating Bluetooth discovery service");
93     }
94
95     @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
96     protected void addBluetoothAdapter(BluetoothAdapter adapter) {
97         this.adapters.add(adapter);
98         adapter.addDiscoveryListener(this);
99     }
100
101     protected void removeBluetoothAdapter(BluetoothAdapter adapter) {
102         this.adapters.remove(adapter);
103         adapter.removeDiscoveryListener(this);
104     }
105
106     @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
107     protected void addBluetoothDiscoveryParticipant(BluetoothDiscoveryParticipant participant) {
108         this.participants.add(participant);
109         supportedThingTypes.addAll(participant.getSupportedThingTypeUIDs());
110     }
111
112     protected void removeBluetoothDiscoveryParticipant(BluetoothDiscoveryParticipant participant) {
113         supportedThingTypes.removeAll(participant.getSupportedThingTypeUIDs());
114         this.participants.remove(participant);
115     }
116
117     @Override
118     public Set<ThingTypeUID> getSupportedThingTypes() {
119         return supportedThingTypes;
120     }
121
122     @Override
123     public void startScan() {
124         for (BluetoothAdapter adapter : adapters) {
125             adapter.scanStart();
126         }
127     }
128
129     @Override
130     public void stopScan() {
131         for (BluetoothAdapter adapter : adapters) {
132             adapter.scanStop();
133         }
134         removeOlderResults(getTimestampOfLastScan());
135     }
136
137     @Override
138     public void deviceRemoved(BluetoothDevice device) {
139         discoveryCaches.computeIfPresent(device.getAddress(), (addr, cache) -> cache.removeDiscoveries(device));
140     }
141
142     @Override
143     public void deviceDiscovered(BluetoothDevice device) {
144         logger.debug("Discovered bluetooth device '{}': {}", device.getName(), device);
145
146         DiscoveryCache cache = discoveryCaches.computeIfAbsent(device.getAddress(), addr -> new DiscoveryCache());
147         cache.handleDiscovery(device);
148     }
149
150     private static ThingUID createThingUIDWithBridge(DiscoveryResult result, BluetoothAdapter adapter) {
151         return new ThingUID(result.getThingTypeUID(), adapter.getUID(), result.getThingUID().getId());
152     }
153
154     private static DiscoveryResult copyWithNewBridge(DiscoveryResult result, BluetoothAdapter adapter) {
155         String label = result.getLabel();
156         String adapterLabel = adapter.getLabel();
157         if (adapterLabel != null) {
158             label = adapterLabel + " - " + label;
159         }
160
161         return DiscoveryResultBuilder.create(createThingUIDWithBridge(result, adapter))//
162                 .withBridge(adapter.getUID())//
163                 .withProperties(result.getProperties())//
164                 .withRepresentationProperty(result.getRepresentationProperty())//
165                 .withTTL(result.getTimeToLive())//
166                 .withLabel(label)//
167                 .build();
168     }
169
170     private class DiscoveryCache {
171
172         private final Map<BluetoothAdapter, SnapshotFuture> discoveryFutures = new HashMap<>();
173         private final Map<BluetoothAdapter, Set<DiscoveryResult>> discoveryResults = new ConcurrentHashMap<>();
174
175         private @Nullable BluetoothDeviceSnapshot latestSnapshot;
176
177         /**
178          * This is meant to be used as part of a Map.compute function
179          *
180          * @param device the device to remove from this cache
181          * @return this DiscoveryCache if there are still snapshots, null otherwise
182          */
183         public synchronized @Nullable DiscoveryCache removeDiscoveries(final BluetoothDevice device) {
184             // we remove any discoveries that have been published for this device
185             BluetoothAdapter adapter = device.getAdapter();
186             if (discoveryFutures.containsKey(adapter)) {
187                 discoveryFutures.remove(adapter).future.thenAccept(result -> retractDiscoveryResult(adapter, result));
188             }
189             if (discoveryFutures.isEmpty()) {
190                 return null;
191             }
192             return this;
193         }
194
195         public synchronized void handleDiscovery(BluetoothDevice device) {
196             if (!discoveryFutures.isEmpty()) {
197                 CompletableFuture
198                         // we have an ongoing futures so lets create our discovery after they all finish
199                         .allOf(discoveryFutures.values().stream().map(sf -> sf.future)
200                                 .toArray(CompletableFuture[]::new))
201                         .thenRun(() -> createDiscoveryFuture(device));
202             } else {
203                 createDiscoveryFuture(device);
204             }
205         }
206
207         private synchronized void createDiscoveryFuture(BluetoothDevice device) {
208             BluetoothAdapter adapter = device.getAdapter();
209             CompletableFuture<DiscoveryResult> future = null;
210
211             BluetoothDeviceSnapshot snapshot = new BluetoothDeviceSnapshot(device);
212             BluetoothDeviceSnapshot latestSnapshot = this.latestSnapshot;
213             if (latestSnapshot != null) {
214                 snapshot.merge(latestSnapshot);
215
216                 if (snapshot.equals(latestSnapshot)) {
217                     // this means that snapshot has no newer fields than the latest snapshot
218                     if (discoveryFutures.containsKey(adapter)
219                             && discoveryFutures.get(adapter).snapshot.equals(latestSnapshot)) {
220                         // This adapter has already produced the most up-to-date result, so no further processing is
221                         // necessary
222                         return;
223                     }
224
225                     /*
226                      * This isn't a new snapshot, but an up-to-date result from this adapter has not been produced yet.
227                      * Since a result must have been produced for this snapshot, we search the results of the other
228                      * adapters to find the future for the latest snapshot, then we modify it to make it look like it
229                      * came from this adapter. This way we don't need to recompute the DiscoveryResult.
230                      */
231                     Optional<CompletableFuture<DiscoveryResult>> otherFuture = discoveryFutures.values().stream()
232                             // make sure that we only get futures for the current snapshot
233                             .filter(sf -> sf.snapshot.equals(latestSnapshot)).findAny().map(sf -> sf.future);
234                     if (otherFuture.isPresent()) {
235                         future = otherFuture.get();
236                     }
237                 }
238             }
239             this.latestSnapshot = snapshot;
240
241             if (future == null) {
242                 // we pass in the snapshot since it acts as a delegate for the device. It will also retain any new
243                 // fields added to the device as part of the discovery process.
244                 future = startDiscoveryProcess(snapshot);
245             }
246
247             if (discoveryFutures.containsKey(adapter)) {
248                 // now we need to make sure that we remove the old discovered result if it is different from the new
249                 // one.
250                 SnapshotFuture oldSF = discoveryFutures.get(adapter);
251                 future = oldSF.future.thenCombine(future, (oldResult, newResult) -> {
252                     logger.trace("\n old: {}\n new: {}", oldResult, newResult);
253                     if (!oldResult.getThingUID().equals(newResult.getThingUID())) {
254                         retractDiscoveryResult(adapter, oldResult);
255                     }
256                     return newResult;
257                 });
258             }
259             /*
260              * this appends a post-process to any ongoing or completed discoveries with this device's address.
261              * If this discoveryFuture is ongoing then this post-process will run asynchronously upon the future's
262              * completion.
263              * If this discoveryFuture is already completed then this post-process will run in the current thread.
264              * We need to make sure that this is part of the future chain so that the call to 'thingRemoved'
265              * in the 'removeDiscoveries' method above can be sure that it is running after the 'thingDiscovered'
266              */
267             future = future.thenApply(result -> {
268                 publishDiscoveryResult(adapter, result);
269                 return result;
270             }).whenComplete((r, t) -> {
271                 if (t != null) {
272                     logger.warn("Error occured during discovery of {}", device.getAddress(), t);
273                 }
274             });
275
276             // now save this snapshot for later
277             discoveryFutures.put(adapter, new SnapshotFuture(snapshot, future));
278         }
279
280         private void publishDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult result) {
281             Set<DiscoveryResult> results = new HashSet<>();
282             BiConsumer<BluetoothAdapter, DiscoveryResult> publisher = (a, r) -> {
283                 results.add(copyWithNewBridge(r, a));
284             };
285
286             publisher.accept(adapter, result);
287             for (BluetoothDiscoveryParticipant participant : participants) {
288                 participant.publishAdditionalResults(result, publisher);
289             }
290             results.forEach(BluetoothDiscoveryService.this::thingDiscovered);
291             discoveryResults.put(adapter, results);
292         }
293
294         private void retractDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult result) {
295             Set<DiscoveryResult> results = discoveryResults.remove(adapter);
296             if (results != null) {
297                 for (DiscoveryResult r : results) {
298                     thingRemoved(r.getThingUID());
299                 }
300             }
301         }
302
303         private CompletableFuture<DiscoveryResult> startDiscoveryProcess(BluetoothDeviceSnapshot device) {
304             return CompletableFuture.supplyAsync(new BluetoothDiscoveryProcess(device, participants, adapters),
305                     scheduler);
306         }
307     }
308
309     private static class SnapshotFuture {
310         public final BluetoothDeviceSnapshot snapshot;
311         public final CompletableFuture<DiscoveryResult> future;
312
313         public SnapshotFuture(BluetoothDeviceSnapshot snapshot, CompletableFuture<DiscoveryResult> future) {
314             this.snapshot = snapshot;
315             this.future = future;
316         }
317     }
318 }