2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.govee.internal;
15 import java.util.UUID;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.Future;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledThreadPoolExecutor;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.TimeoutException;
22 import java.util.concurrent.locks.Condition;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
27 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
28 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
29 import org.openhab.binding.bluetooth.BluetoothDescriptor;
30 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
31 import org.openhab.binding.bluetooth.BluetoothService;
32 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
33 import org.openhab.core.common.NamedThreadFactory;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.util.HexUtils;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
44 * @author Kai Kreuzer - Initial contribution and API
45 * @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
49 public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
51 private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
53 private final Condition connectionCondition = deviceLock.newCondition();
54 private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
55 private final Condition charCompleteCondition = deviceLock.newCondition();
57 private @Nullable Future<?> reconnectJob;
58 private @Nullable Future<?> pendingDisconnect;
59 private @Nullable BluetoothCharacteristic ongoingCharacteristic;
60 private @Nullable BluetoothCompletionStatus completeStatus;
62 private boolean connectOnDemand;
63 private int idleDisconnectDelayMs = 1000;
65 protected @Nullable ScheduledExecutorService connectionTaskExecutor;
66 private volatile boolean servicesDiscovered;
68 public ConnectedBluetoothHandler(Thing thing) {
73 public void initialize() {
75 // super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
76 // the connectionTaskExecutor first
77 ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
78 new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true));
79 executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
80 executor.setRemoveOnCancelPolicy(true);
81 connectionTaskExecutor = executor;
85 connectOnDemand = true;
87 Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
88 idleDisconnectDelayMs = 1000;
89 if (idleDisconnectDelayRaw instanceof Number) {
90 idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
93 if (!connectOnDemand) {
94 reconnectJob = executor.scheduleWithFixedDelay(() -> {
96 if (device.getConnectionState() != ConnectionState.CONNECTED) {
98 // we do not set the Thing status here, because we will anyhow receive a call to
99 // onConnectionStateChange
101 // just in case it was already connected to begin with
102 updateStatus(ThingStatus.ONLINE);
103 if (!servicesDiscovered && !device.discoverServices()) {
104 logger.debug("Error while discovering services");
107 } catch (RuntimeException ex) {
108 logger.warn("Unexpected error occurred", ex);
110 }, 0, 30, TimeUnit.SECONDS);
115 public void dispose() {
116 cancel(reconnectJob);
118 cancel(pendingDisconnect);
119 pendingDisconnect = null;
123 shutdown(connectionTaskExecutor);
124 connectionTaskExecutor = null;
127 private static void cancel(@Nullable Future<?> future) {
128 if (future != null) {
133 private void shutdown(@Nullable ScheduledExecutorService executor) {
134 if (executor != null) {
135 executor.shutdownNow();
139 private ScheduledExecutorService getConnectionTaskExecutor() {
140 var executor = connectionTaskExecutor;
141 if (executor == null) {
142 throw new IllegalStateException("characteristicScheduler has not been initialized");
147 private void scheduleDisconnect() {
148 cancel(pendingDisconnect);
149 pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
150 TimeUnit.MILLISECONDS);
153 private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
154 if (device.getConnectionState() == ConnectionState.CONNECTED) {
157 if (device.getConnectionState() != ConnectionState.CONNECTING) {
158 if (!device.connect()) {
159 throw new ConnectionException("Failed to start connecting");
162 logger.debug("waiting for connection");
163 if (!awaitConnection(1, TimeUnit.SECONDS)) {
164 throw new TimeoutException("Connection attempt timeout.");
166 logger.debug("connection successful");
167 if (!servicesDiscovered) {
168 logger.debug("discovering services");
169 device.discoverServices();
170 if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
171 throw new TimeoutException("Service discovery timeout");
173 logger.debug("service discovery successful");
177 private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
180 long nanosTimeout = unit.toNanos(timeout);
181 while (device.getConnectionState() != ConnectionState.CONNECTED) {
182 if (nanosTimeout <= 0L) {
185 nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
193 private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
196 long nanosTimeout = unit.toNanos(timeout);
197 while (ongoingCharacteristic != null) {
198 if (nanosTimeout <= 0L) {
201 nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
209 private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
212 long nanosTimeout = unit.toNanos(timeout);
213 while (!servicesDiscovered) {
214 if (nanosTimeout <= 0L) {
217 nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
225 private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
226 throws BluetoothException, TimeoutException, InterruptedException {
228 BluetoothService service = device.getServices(serviceUUID);
229 if (service == null) {
230 throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
232 BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
233 if (characteristic == null) {
234 throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
236 return characteristic;
239 private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
240 CallableFunction<BluetoothCharacteristic, T> callable) {
241 CompletableFuture<T> future = new CompletableFuture<>();
242 var executor = connectionTaskExecutor;
243 if (executor != null) {
244 executor.execute(() -> {
245 cancel(pendingDisconnect);
247 BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
249 future.complete(callable.call(characteristic));
250 } catch (InterruptedException e) {
251 future.completeExceptionally(e);
252 return;// we don't want to schedule anything if we receive an interrupt
253 } catch (TimeoutException e) {
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
255 future.completeExceptionally(e);
256 } catch (Exception e) {
257 future.completeExceptionally(e);
259 if (connectOnDemand) {
260 scheduleDisconnect();
264 future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
269 public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
270 return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
271 if (!device.enableNotifications(characteristic)) {
272 throw new BluetoothException(
273 "Failed to start notifications for characteristic: " + characteristic.getUuid());
279 public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
280 boolean enableNotification) {
281 return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
282 if (enableNotification) {
283 if (!device.enableNotifications(characteristic)) {
284 throw new BluetoothException(
285 "Failed to start characteristic notification" + characteristic.getUuid());
288 // now block for completion
289 characteristic.setValue(data);
290 ongoingCharacteristic = characteristic;
291 if (!device.writeCharacteristic(characteristic)) {
292 throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
294 if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
295 ongoingCharacteristic = null;
296 throw new TimeoutException(
297 "Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
299 if (completeStatus == BluetoothCompletionStatus.ERROR) {
300 throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
302 logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
303 characteristic.getUuid(), address);
308 public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
309 return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
310 // now block for completion
311 ongoingCharacteristic = characteristic;
312 if (!device.readCharacteristic(characteristic)) {
313 throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
315 if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
316 ongoingCharacteristic = null;
317 throw new TimeoutException(
318 "Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
320 if (completeStatus == BluetoothCompletionStatus.ERROR) {
321 throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
323 byte[] data = characteristic.getByteValue();
324 logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
325 HexUtils.bytesToHex(data));
331 protected void updateStatusBasedOnRssi(boolean receivedSignal) {
332 // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
334 if (receivedSignal) {
335 if (device.getConnectionState() == ConnectionState.CONNECTED) {
336 updateStatus(ThingStatus.ONLINE);
338 if (!connectOnDemand) {
339 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
343 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
348 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
349 super.onConnectionStateChange(connectionNotification);
350 switch (connectionNotification.getConnectionState()) {
352 // The device is now known on the Bluetooth network, so we can do something...
353 if (!connectOnDemand) {
354 getConnectionTaskExecutor().submit(() -> {
355 if (device.getConnectionState() != ConnectionState.CONNECTED) {
356 if (!device.connect()) {
357 logger.debug("Error connecting to device after discovery.");
366 connectionCondition.signal();
370 if (!connectOnDemand) {
371 getConnectionTaskExecutor().submit(() -> {
372 if (!servicesDiscovered && !device.discoverServices()) {
373 logger.debug("Error while discovering services");
379 var future = pendingDisconnect;
380 if (future != null) {
381 future.cancel(false);
383 if (!connectOnDemand) {
384 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
393 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
394 super.onCharacteristicReadComplete(characteristic, status);
397 if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
398 completeStatus = status;
399 ongoingCharacteristic = null;
400 charCompleteCondition.signal();
408 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
409 BluetoothCompletionStatus status) {
410 super.onCharacteristicWriteComplete(characteristic, status);
413 if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
414 completeStatus = status;
415 ongoingCharacteristic = null;
416 charCompleteCondition.signal();
424 public void onServicesDiscovered() {
425 super.onServicesDiscovered();
428 this.servicesDiscovered = true;
429 serviceDiscoveryCondition.signal();
433 logger.debug("Service discovery completed for '{}'", address);
437 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
438 super.onCharacteristicUpdate(characteristic);
439 if (logger.isDebugEnabled()) {
440 logger.debug("Recieved update {} to characteristic {} of device {}",
441 HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
446 public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
447 super.onDescriptorUpdate(descriptor);
448 if (logger.isDebugEnabled()) {
449 logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
450 descriptor.getUuid(), address);
454 public static class BluetoothException extends Exception {
456 public BluetoothException(String message) {
461 public static class ConnectionException extends BluetoothException {
463 public ConnectionException(String message) {
469 public static interface CallableFunction<U, R> {
470 public R call(U arg) throws Exception;