2 * Copyright (c) 2010-2023 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;
15 import java.util.Collection;
16 import java.util.UUID;
17 import java.util.concurrent.CompletableFuture;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
23 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
28 * The {@link BluetoothDevice} class provides a base implementation of a Bluetooth Low Energy device
30 * @author Chris Jackson - Initial contribution
31 * @author Kai Kreuzer - Refactored class to use Integer instead of int, fixed bugs, diverse improvements
32 * @author Connor Petty - Made most of the methods abstract
33 * @author Peter Rosenberg - Improve notifications
36 public abstract class BluetoothDevice {
38 private final Logger logger = LoggerFactory.getLogger(BluetoothDevice.class);
41 * Enumeration of Bluetooth connection states
44 public enum ConnectionState {
46 * Device is still being discovered and is not available for use.
50 * Device has been discovered. This is used for the initial notification that the device is available.
54 * Device is disconnected.
58 * A connection is in progress.
62 * The device is connected.
66 * A disconnection is in progress.
71 protected enum BluetoothEventType {
74 CHARACTERISTIC_UPDATED,
81 * The adapter the device is accessed through
83 protected final BluetoothAdapter adapter;
86 * Devices Bluetooth address
88 protected final BluetoothAddress address;
91 * Construct a Bluetooth device taking the Bluetooth address
96 public BluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
97 this.address = address;
98 this.adapter = adapter;
102 * Returns the name of the Bluetooth device.
104 * @return The devices name
106 public abstract @Nullable String getName();
109 * Returns the manufacturer ID of the device
111 * @return an integer with manufacturer ID of the device, or null if not known
113 public abstract @Nullable Integer getManufacturerId();
116 * Returns the last Transmit Power value or null if no transmit power has been received
118 * @return the last reported transmitter power value in dBm
120 public abstract @Nullable Integer getTxPower();
123 * Returns the last Receive Signal Strength Indicator (RSSI) value or null if no RSSI has been received
125 * @return the last RSSI value in dBm
127 public abstract @Nullable Integer getRssi();
130 * Returns the physical address of the device.
132 * @return The physical address of the device
134 public BluetoothAddress getAddress() {
139 * Returns the adapter through which the device is accessed
141 * @return The adapter through which the device is accessed
143 public BluetoothAdapter getAdapter() {
148 * Returns a {@link BluetoothService} if the requested service is supported
150 * @return the {@link BluetoothService} or null if the service is not supported.
152 public abstract @Nullable BluetoothService getServices(UUID uuid);
155 * Returns a list of supported service UUIDs
157 * @return list of supported {@link BluetoothService}s.
159 public abstract Collection<BluetoothService> getServices();
162 * Check if the device supports the specified service
164 * @param uuid the service {@link UUID}
165 * @return true if the service is supported
167 public abstract boolean supportsService(UUID uuid);
170 * Get the current connection state for this device
172 * @return the current {@link ConnectionState}
174 public abstract ConnectionState getConnectionState();
177 * Connects to a device. This is an asynchronous method. Once the connection state is updated, the
178 * {@link BluetoothDeviceListener.onConnectionState} method will be called with the connection state.
180 * If the device is already connected, this will return false.
182 * @return true if the connection process is started successfully
184 public abstract boolean connect();
187 * Disconnects from a device. Once the connection state is updated, the
188 * {@link BluetoothDeviceListener.onConnectionState}
189 * method will be called with the connection state.
191 * If the device is not currently connected, this will return false.
193 * @return true if the disconnection process is started successfully
195 public abstract boolean disconnect();
198 * Starts a discovery on a device. This will iterate through all services and characteristics to build up a view of
201 * This method should be called before attempting to read or write characteristics.
203 * @return true if the discovery process is started successfully
205 public abstract boolean discoverServices();
208 * Gets a Bluetooth characteristic if it is known.
210 * Note that this method will not search for a characteristic in the remote device if it is not known.
211 * You must have previously connected to the device so that the device services and characteristics can
214 * @param uuid the {@link UUID} of the characteristic to return
215 * @return the {@link BluetoothCharacteristic} or null if the characteristic is not found in the device
217 public @Nullable BluetoothCharacteristic getCharacteristic(UUID uuid) {
218 for (BluetoothService service : getServices()) {
219 if (service.providesCharacteristic(uuid)) {
220 return service.getCharacteristic(uuid);
227 * Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
228 * operation when one is already in progress will result in subsequent calls returning false.
230 * This is an asynchronous method. Once the read is complete the returned future will be updated with the result.
232 * @param characteristic the {@link BluetoothCharacteristic} to read.
233 * @return a future that returns the read data is successful, otherwise throws an exception
235 public abstract CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic);
238 * Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
239 * operation when one is already in progress will result in subsequent calls returning false.
241 * This is an asynchronous method. Once the write is complete the returned future will be updated with the result.
243 * @param characteristic the {@link BluetoothCharacteristic} to write.
244 * @param value the data to write
245 * @return a future that returns null upon a successful write, otherwise throws an exception
247 public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic,
251 * Returns if notification is enabled for the given characteristic.
253 * @param characteristic the {@link BluetoothCharacteristic} to check if notifications are enabled.
254 * @return true if notification is enabled, false if notification is disabled, characteristic is missing on device
255 * or notifications are not supported.
257 public abstract boolean isNotifying(BluetoothCharacteristic characteristic);
260 * Enables notifications for a characteristic. Only a single read or write operation can be requested at once.
261 * Attempting to perform an operation when one is already in progress will result in subsequent calls returning
264 * Notifications result in CHARACTERISTIC_UPDATED events to the listeners.
266 * @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
267 * @return true if the characteristic notification is started successfully
269 public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic);
272 * Disables notifications for a characteristic. Only a single read or write operation can be requested at once.
273 * Attempting to perform an operation when one is already in progress will result in subsequent calls returning
276 * @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
277 * @return true if the characteristic notification is stopped successfully
279 public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic);
282 * Enables notifications for a descriptor. Only a single read or write operation can be requested at once.
283 * Attempting to perform an operation when one is already in progress will result in subsequent calls returning
286 * Notifications result in DESCRIPTOR_UPDATED events to the listeners.
288 * @param descriptor the {@link BluetoothDescriptor} to receive notifications for.
289 * @return true if the descriptor notification is started successfully
291 public abstract boolean enableNotifications(BluetoothDescriptor descriptor);
294 * Disables notifications for a descriptor. Only a single read or write operation can be requested at once.
295 * Attempting to perform an operation when one is already in progress will result in subsequent calls returning
298 * @param descriptor the {@link BluetoothDescriptor} to disable notifications for.
299 * @return true if the descriptor notification is stopped successfully
301 public abstract boolean disableNotifications(BluetoothDescriptor descriptor);
304 * Adds a service to the device.
306 * @param service the new {@link BluetoothService} to add
307 * @return true if the service was added or false if the service was already supported
309 protected abstract boolean addService(BluetoothService service);
312 * Gets a service based on the handle.
313 * This will return a service if the handle falls within the start and end handles for the service.
315 * @param handle the handle for the service
316 * @return the {@link BluetoothService} or null if the service was not found
318 protected @Nullable BluetoothService getServiceByHandle(int handle) {
319 for (BluetoothService service : getServices()) {
320 if (service.getHandleStart() <= handle && service.getHandleEnd() >= handle) {
328 * Gets a characteristic based on the handle.
330 * @param handle the handle for the characteristic
331 * @return the {@link BluetoothCharacteristic} or null if the characteristic was not found
333 protected @Nullable BluetoothCharacteristic getCharacteristicByHandle(int handle) {
334 BluetoothService service = getServiceByHandle(handle);
335 if (service != null) {
336 return service.getCharacteristicByHandle(handle);
342 * Adds a device listener
344 * @param listener the {@link BluetoothDeviceListener} to add
346 public final void addListener(BluetoothDeviceListener listener) {
347 getListeners().add(listener);
351 * Removes a device listener
353 * @param listener the {@link BluetoothDeviceListener} to remove
355 public final void removeListener(BluetoothDeviceListener listener) {
356 getListeners().remove(listener);
360 * Checks if this device has any listeners
362 * @return true if this device has listeners
364 public final boolean hasListeners() {
365 return !getListeners().isEmpty();
369 * Releases resources that this device is using.
372 protected abstract void dispose();
374 protected abstract Collection<BluetoothDeviceListener> getListeners();
376 public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException;
378 public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException;
380 public abstract boolean isServicesDiscovered();
383 * Notify the listeners of an event
385 * @param event the {@link BluetoothEventType} of this event
386 * @param args an array of arguments to pass to the callback
388 protected void notifyListeners(BluetoothEventType event, Object... args) {
389 for (BluetoothDeviceListener listener : getListeners()) {
393 listener.onScanRecordReceived((BluetoothScanNotification) args[0]);
395 case CONNECTION_STATE:
396 listener.onConnectionStateChange((BluetoothConnectionStatusNotification) args[0]);
398 case SERVICES_DISCOVERED:
399 listener.onServicesDiscovered();
401 case CHARACTERISTIC_UPDATED:
402 listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]);
404 case DESCRIPTOR_UPDATED:
405 listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]);
407 case ADAPTER_CHANGED:
408 listener.onAdapterChanged((BluetoothAdapter) args[0]);
411 } catch (Exception e) {
412 logger.error("Failed to inform listener '{}': {}", listener, e.getMessage(), e);