]> git.basschouten.com Git - openhab-addons.git/blob
1cff9dbe27494fe9bc2f2941e58b26b7be33fa63
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.Collection;
16 import java.util.UUID;
17 import java.util.concurrent.CompletableFuture;
18 import java.util.concurrent.TimeUnit;
19
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;
26
27 /**
28  * The {@link BluetoothDevice} class provides a base implementation of a Bluetooth Low Energy device
29  *
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
34  */
35 @NonNullByDefault
36 public abstract class BluetoothDevice {
37
38     private final Logger logger = LoggerFactory.getLogger(BluetoothDevice.class);
39
40     /**
41      * Enumeration of Bluetooth connection states
42      *
43      */
44     public enum ConnectionState {
45         /**
46          * Device is still being discovered and is not available for use.
47          */
48         DISCOVERING,
49         /**
50          * Device has been discovered. This is used for the initial notification that the device is available.
51          */
52         DISCOVERED,
53         /**
54          * Device is disconnected.
55          */
56         DISCONNECTED,
57         /**
58          * A connection is in progress.
59          */
60         CONNECTING,
61         /**
62          * The device is connected.
63          */
64         CONNECTED,
65         /**
66          * A disconnection is in progress.
67          */
68         DISCONNECTING
69     }
70
71     protected enum BluetoothEventType {
72         CONNECTION_STATE,
73         SCAN_RECORD,
74         CHARACTERISTIC_UPDATED,
75         DESCRIPTOR_UPDATED,
76         SERVICES_DISCOVERED,
77         ADAPTER_CHANGED
78     }
79
80     /**
81      * The adapter the device is accessed through
82      */
83     protected final BluetoothAdapter adapter;
84
85     /**
86      * Devices Bluetooth address
87      */
88     protected final BluetoothAddress address;
89
90     /**
91      * Construct a Bluetooth device taking the Bluetooth address
92      *
93      * @param adapter
94      * @param sender
95      */
96     public BluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
97         this.address = address;
98         this.adapter = adapter;
99     }
100
101     /**
102      * Returns the name of the Bluetooth device.
103      *
104      * @return The devices name
105      */
106     public abstract @Nullable String getName();
107
108     /**
109      * Returns the manufacturer ID of the device
110      *
111      * @return an integer with manufacturer ID of the device, or null if not known
112      */
113     public abstract @Nullable Integer getManufacturerId();
114
115     /**
116      * Returns the last Transmit Power value or null if no transmit power has been received
117      *
118      * @return the last reported transmitter power value in dBm
119      */
120     public abstract @Nullable Integer getTxPower();
121
122     /**
123      * Returns the last Receive Signal Strength Indicator (RSSI) value or null if no RSSI has been received
124      *
125      * @return the last RSSI value in dBm
126      */
127     public abstract @Nullable Integer getRssi();
128
129     /**
130      * Returns the physical address of the device.
131      *
132      * @return The physical address of the device
133      */
134     public BluetoothAddress getAddress() {
135         return address;
136     }
137
138     /**
139      * Returns the adapter through which the device is accessed
140      *
141      * @return The adapter through which the device is accessed
142      */
143     public BluetoothAdapter getAdapter() {
144         return adapter;
145     }
146
147     /**
148      * Returns a {@link BluetoothService} if the requested service is supported
149      *
150      * @return the {@link BluetoothService} or null if the service is not supported.
151      */
152     public abstract @Nullable BluetoothService getServices(UUID uuid);
153
154     /**
155      * Returns a list of supported service UUIDs
156      *
157      * @return list of supported {@link BluetoothService}s.
158      */
159     public abstract Collection<BluetoothService> getServices();
160
161     /**
162      * Check if the device supports the specified service
163      *
164      * @param uuid the service {@link UUID}
165      * @return true if the service is supported
166      */
167     public abstract boolean supportsService(UUID uuid);
168
169     /**
170      * Get the current connection state for this device
171      *
172      * @return the current {@link ConnectionState}
173      */
174     public abstract ConnectionState getConnectionState();
175
176     /**
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.
179      * <p>
180      * If the device is already connected, this will return false.
181      *
182      * @return true if the connection process is started successfully
183      */
184     public abstract boolean connect();
185
186     /**
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.
190      * <p>
191      * If the device is not currently connected, this will return false.
192      *
193      * @return true if the disconnection process is started successfully
194      */
195     public abstract boolean disconnect();
196
197     /**
198      * Starts a discovery on a device. This will iterate through all services and characteristics to build up a view of
199      * the device.
200      * <p>
201      * This method should be called before attempting to read or write characteristics.
202      *
203      * @return true if the discovery process is started successfully
204      */
205     public abstract boolean discoverServices();
206
207     /**
208      * Gets a Bluetooth characteristic if it is known.
209      * <p>
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
212      * be retrieved.
213      *
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
216      */
217     public @Nullable BluetoothCharacteristic getCharacteristic(UUID uuid) {
218         for (BluetoothService service : getServices()) {
219             if (service.providesCharacteristic(uuid)) {
220                 return service.getCharacteristic(uuid);
221             }
222         }
223         return null;
224     }
225
226     /**
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.
229      * <p>
230      * This is an asynchronous method. Once the read is complete the returned future will be updated with the result.
231      *
232      * @param characteristic the {@link BluetoothCharacteristic} to read.
233      * @return a future that returns the read data is successful, otherwise throws an exception
234      */
235     public abstract CompletableFuture<byte[]> readCharacteristic(BluetoothCharacteristic characteristic);
236
237     /**
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.
240      * <p>
241      * This is an asynchronous method. Once the write is complete the returned future will be updated with the result.
242      *
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
246      */
247     public abstract CompletableFuture<@Nullable Void> writeCharacteristic(BluetoothCharacteristic characteristic,
248             byte[] value);
249
250     /**
251      * Returns if notification is enabled for the given characteristic.
252      *
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.
256      */
257     public abstract boolean isNotifying(BluetoothCharacteristic characteristic);
258
259     /**
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
262      * false.
263      * <p>
264      * Notifications result in CHARACTERISTIC_UPDATED events to the listeners.
265      *
266      * @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
267      * @return true if the characteristic notification is started successfully
268      */
269     public abstract CompletableFuture<@Nullable Void> enableNotifications(BluetoothCharacteristic characteristic);
270
271     /**
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
274      * false.
275      *
276      * @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
277      * @return true if the characteristic notification is stopped successfully
278      */
279     public abstract CompletableFuture<@Nullable Void> disableNotifications(BluetoothCharacteristic characteristic);
280
281     /**
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
284      * false.
285      * <p>
286      * Notifications result in DESCRIPTOR_UPDATED events to the listeners.
287      *
288      * @param descriptor the {@link BluetoothDescriptor} to receive notifications for.
289      * @return true if the descriptor notification is started successfully
290      */
291     public abstract boolean enableNotifications(BluetoothDescriptor descriptor);
292
293     /**
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
296      * false.
297      *
298      * @param descriptor the {@link BluetoothDescriptor} to disable notifications for.
299      * @return true if the descriptor notification is stopped successfully
300      */
301     public abstract boolean disableNotifications(BluetoothDescriptor descriptor);
302
303     /**
304      * Adds a service to the device.
305      *
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
308      */
309     protected abstract boolean addService(BluetoothService service);
310
311     /**
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.
314      *
315      * @param handle the handle for the service
316      * @return the {@link BluetoothService} or null if the service was not found
317      */
318     protected @Nullable BluetoothService getServiceByHandle(int handle) {
319         for (BluetoothService service : getServices()) {
320             if (service.getHandleStart() <= handle && service.getHandleEnd() >= handle) {
321                 return service;
322             }
323         }
324         return null;
325     }
326
327     /**
328      * Gets a characteristic based on the handle.
329      *
330      * @param handle the handle for the characteristic
331      * @return the {@link BluetoothCharacteristic} or null if the characteristic was not found
332      */
333     protected @Nullable BluetoothCharacteristic getCharacteristicByHandle(int handle) {
334         BluetoothService service = getServiceByHandle(handle);
335         if (service != null) {
336             return service.getCharacteristicByHandle(handle);
337         }
338         return null;
339     }
340
341     /**
342      * Adds a device listener
343      *
344      * @param listener the {@link BluetoothDeviceListener} to add
345      */
346     public final void addListener(BluetoothDeviceListener listener) {
347         getListeners().add(listener);
348     }
349
350     /**
351      * Removes a device listener
352      *
353      * @param listener the {@link BluetoothDeviceListener} to remove
354      */
355     public final void removeListener(BluetoothDeviceListener listener) {
356         getListeners().remove(listener);
357     }
358
359     /**
360      * Checks if this device has any listeners
361      *
362      * @return true if this device has listeners
363      */
364     public final boolean hasListeners() {
365         return !getListeners().isEmpty();
366     }
367
368     /**
369      * Releases resources that this device is using.
370      *
371      */
372     protected abstract void dispose();
373
374     protected abstract Collection<BluetoothDeviceListener> getListeners();
375
376     public abstract boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException;
377
378     public abstract boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException;
379
380     public abstract boolean isServicesDiscovered();
381
382     /**
383      * Notify the listeners of an event
384      *
385      * @param event the {@link BluetoothEventType} of this event
386      * @param args an array of arguments to pass to the callback
387      */
388     protected void notifyListeners(BluetoothEventType event, Object... args) {
389         for (BluetoothDeviceListener listener : getListeners()) {
390             try {
391                 switch (event) {
392                     case SCAN_RECORD:
393                         listener.onScanRecordReceived((BluetoothScanNotification) args[0]);
394                         break;
395                     case CONNECTION_STATE:
396                         listener.onConnectionStateChange((BluetoothConnectionStatusNotification) args[0]);
397                         break;
398                     case SERVICES_DISCOVERED:
399                         listener.onServicesDiscovered();
400                         break;
401                     case CHARACTERISTIC_UPDATED:
402                         listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0], (byte[]) args[1]);
403                         break;
404                     case DESCRIPTOR_UPDATED:
405                         listener.onDescriptorUpdate((BluetoothDescriptor) args[0], (byte[]) args[1]);
406                         break;
407                     case ADAPTER_CHANGED:
408                         listener.onAdapterChanged((BluetoothAdapter) args[0]);
409                         break;
410                 }
411             } catch (Exception e) {
412                 logger.error("Failed to inform listener '{}': {}", listener, e.getMessage(), e);
413             }
414         }
415     }
416 }