]> git.basschouten.com Git - openhab-addons.git/blob
556b23ee2f11bee3401dff367a69530cc1b0602d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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;
14
15 import java.util.UUID;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.CompletionException;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.Future;
20 import java.util.concurrent.ScheduledExecutorService;
21 import java.util.concurrent.ScheduledThreadPoolExecutor;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.function.Function;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
29 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
30 import org.openhab.binding.bluetooth.util.RetryFuture;
31 import org.openhab.core.common.NamedThreadFactory;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.util.HexUtils;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
41  *
42  * @author Kai Kreuzer - Initial contribution and API
43  */
44 @NonNullByDefault
45 public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
46
47     private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
48     private @Nullable Future<?> reconnectJob;
49     private @Nullable Future<?> pendingDisconnect;
50
51     private boolean alwaysConnected;
52     private int idleDisconnectDelay = 1000;
53
54     // we initially set the to scheduler so that we can keep this field non-null
55     private ScheduledExecutorService connectionTaskExecutor = scheduler;
56
57     public ConnectedBluetoothHandler(Thing thing) {
58         super(thing);
59     }
60
61     @Override
62     public void initialize() {
63
64         // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
65         // the connectionTaskExecutor first
66         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
67                 new NamedThreadFactory("bluetooth-connection" + thing.getThingTypeUID(), true));
68         executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
69         executor.setRemoveOnCancelPolicy(true);
70         connectionTaskExecutor = executor;
71
72         super.initialize();
73
74         if (thing.getStatus() == ThingStatus.OFFLINE) {
75             // something went wrong in super.initialize() so we shouldn't initialize further here either
76             return;
77         }
78
79         Object alwaysConnectRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_ALWAYS_CONNECTED);
80         alwaysConnected = !Boolean.FALSE.equals(alwaysConnectRaw);
81
82         Object idleDisconnectDelayRaw = getConfig().get(BluetoothBindingConstants.CONFIGURATION_IDLE_DISCONNECT_DELAY);
83         idleDisconnectDelay = 1000;
84         if (idleDisconnectDelayRaw instanceof Number) {
85             idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue();
86         }
87
88         if (alwaysConnected) {
89             reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
90                 try {
91                     if (device.getConnectionState() != ConnectionState.CONNECTED) {
92                         if (!device.connect()) {
93                             logger.debug("Failed to connect to {}", address);
94                         }
95                         // we do not set the Thing status here, because we will anyhow receive a call to
96                         // onConnectionStateChange
97                     } else {
98                         // just in case it was already connected to begin with
99                         updateStatus(ThingStatus.ONLINE);
100                         if (!device.isServicesDiscovered() && !device.discoverServices()) {
101                             logger.debug("Error while discovering services");
102                         }
103                     }
104                 } catch (RuntimeException ex) {
105                     logger.warn("Unexpected error occurred", ex);
106                 }
107             }, 0, 30, TimeUnit.SECONDS);
108         }
109     }
110
111     @Override
112     public void dispose() {
113         cancel(reconnectJob, true);
114         reconnectJob = null;
115         cancel(pendingDisconnect, true);
116         pendingDisconnect = null;
117
118         super.dispose();
119
120         // just in case something goes really wrong in the core and it tries to dispose a handler before initializing it
121         if (scheduler != connectionTaskExecutor) {
122             connectionTaskExecutor.shutdownNow();
123         }
124     }
125
126     private static void cancel(@Nullable Future<?> future, boolean interrupt) {
127         if (future != null) {
128             future.cancel(interrupt);
129         }
130     }
131
132     public void connect() {
133         connectionTaskExecutor.execute(() -> {
134             if (!device.connect()) {
135                 logger.debug("Failed to connect to {}", address);
136             }
137         });
138     }
139
140     public void disconnect() {
141         connectionTaskExecutor.execute(device::disconnect);
142     }
143
144     private void scheduleDisconnect() {
145         cancel(pendingDisconnect, false);
146         pendingDisconnect = connectionTaskExecutor.schedule(device::disconnect, idleDisconnectDelay,
147                 TimeUnit.MILLISECONDS);
148     }
149
150     private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
151         if (device.getConnectionState() == ConnectionState.CONNECTED) {
152             return;
153         }
154         if (device.getConnectionState() != ConnectionState.CONNECTING) {
155             if (!device.connect()) {
156                 throw new ConnectionException("Failed to start connecting");
157             }
158         }
159         if (!device.awaitConnection(1, TimeUnit.SECONDS)) {
160             throw new TimeoutException("Connection attempt timeout.");
161         }
162         if (!device.isServicesDiscovered()) {
163             device.discoverServices();
164             if (!device.awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
165                 throw new TimeoutException("Service discovery timeout");
166             }
167         }
168     }
169
170     private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
171             throws BluetoothException, TimeoutException, InterruptedException {
172         connectAndWait();
173         BluetoothService service = device.getServices(serviceUUID);
174         if (service == null) {
175             throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
176         }
177         BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
178         if (characteristic == null) {
179             throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
180         }
181         return characteristic;
182     }
183
184     private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
185             Function<BluetoothCharacteristic, CompletableFuture<T>> callable) {
186         if (connectionTaskExecutor == scheduler) {
187             return CompletableFuture
188                     .failedFuture(new IllegalStateException("connectionTaskExecutor has not been initialized"));
189         }
190         if (connectionTaskExecutor.isShutdown()) {
191             return CompletableFuture.failedFuture(new IllegalStateException("connectionTaskExecutor is shut down"));
192         }
193         // we use a RetryFuture because it supports running Callable instances
194         return RetryFuture.callWithRetry(() -> {
195             // we block for completion here so that we keep the lock on the connectionTaskExecutor active.
196             return callable.apply(connectAndGetCharacteristic(serviceUUID, characteristicUUID)).get();
197         }, connectionTaskExecutor)// we make this completion async so that operations chained off the returned future
198                                   // will not run on the connectionTaskExecutor
199                 .whenCompleteAsync((r, th) -> {
200                     // we us a while loop here in case the exceptions get nested
201                     while (th instanceof CompletionException || th instanceof ExecutionException) {
202                         th = th.getCause();
203                     }
204                     if (th instanceof InterruptedException) {
205                         // we don't want to schedule anything if we receive an interrupt
206                         return;
207                     }
208                     if (th instanceof TimeoutException) {
209                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, th.getMessage());
210                     }
211                     if (!alwaysConnected) {
212                         scheduleDisconnect();
213                     }
214                 }, scheduler);
215     }
216
217     public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
218         return executeWithConnection(serviceUUID, characteristicUUID, device::enableNotifications);
219     }
220
221     public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
222             boolean enableNotification) {
223         var future = executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
224             if (enableNotification) {
225                 return device.enableNotifications(characteristic)
226                         .thenCompose((v) -> device.writeCharacteristic(characteristic, data));
227             } else {
228                 return device.writeCharacteristic(characteristic, data);
229             }
230         });
231         if (logger.isDebugEnabled()) {
232             future = future.whenComplete((v, t) -> {
233                 if (t == null) {
234                     logger.debug("Characteristic {} from {} has written value {}", characteristicUUID, address,
235                             HexUtils.bytesToHex(data));
236                 }
237             });
238         }
239         return future;
240     }
241
242     public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
243         var future = executeWithConnection(serviceUUID, characteristicUUID, device::readCharacteristic);
244         if (logger.isDebugEnabled()) {
245             future = future.whenComplete((data, t) -> {
246                 if (t == null) {
247                     if (logger.isDebugEnabled()) {
248                         logger.debug("Characteristic {} from {} has been read - value {}", characteristicUUID, address,
249                                 HexUtils.bytesToHex(data));
250                     }
251                 }
252             });
253         }
254         return future;
255     }
256
257     @Override
258     protected void updateStatusBasedOnRssi(boolean receivedSignal) {
259         // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
260         // we are connected.
261         if (receivedSignal) {
262             if (alwaysConnected) {
263                 if (device.getConnectionState() == ConnectionState.CONNECTED) {
264                     updateStatus(ThingStatus.ONLINE);
265                 } else {
266                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
267                 }
268             }
269         } else {
270             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
271         }
272     }
273
274     @Override
275     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
276         super.onConnectionStateChange(connectionNotification);
277         switch (connectionNotification.getConnectionState()) {
278             case DISCOVERED:
279                 // The device is now known on the Bluetooth network, so we can do something...
280                 if (alwaysConnected) {
281                     connectionTaskExecutor.submit(() -> {
282                         if (device.getConnectionState() != ConnectionState.CONNECTED) {
283                             if (!device.connect()) {
284                                 logger.debug("Error connecting to device after discovery.");
285                             }
286                         }
287                     });
288                 }
289                 break;
290             case CONNECTED:
291                 if (alwaysConnected) {
292                     connectionTaskExecutor.submit(() -> {
293                         if (!device.isServicesDiscovered() && !device.discoverServices()) {
294                             logger.debug("Error while discovering services");
295                         }
296                     });
297                 }
298                 break;
299             case DISCONNECTED:
300                 cancel(pendingDisconnect, false);
301                 if (alwaysConnected) {
302                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
303                 }
304                 break;
305             default:
306                 break;
307         }
308     }
309
310     @Override
311     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
312         super.onCharacteristicUpdate(characteristic, value);
313         if (logger.isDebugEnabled()) {
314             logger.debug("Recieved update {} to characteristic {} of device {}", HexUtils.bytesToHex(value),
315                     characteristic.getUuid(), address);
316         }
317     }
318
319     @Override
320     public void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) {
321         super.onDescriptorUpdate(descriptor, value);
322         if (logger.isDebugEnabled()) {
323             logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(value),
324                     descriptor.getUuid(), address);
325         }
326     }
327 }