]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth] Add support for service data (#10278)
authorPete <8108165+PRosenb@users.noreply.github.com>
Thu, 25 Aug 2022 19:36:21 +0000 (05:36 +1000)
committerGitHub <noreply@github.com>
Thu, 25 Aug 2022 19:36:21 +0000 (21:36 +0200)
Signed-off-by: Peter Rosenberg <prosenb.dev@gmail.com>
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServiceDataEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java
bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/notification/BluetoothScanNotification.java

index 137aafbae250ae24ffabb6378f722a82a28fda97..575dda68148d7c7d489f9f1426af220065fd7f18 100644 (file)
@@ -37,6 +37,7 @@ import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
+import org.openhab.binding.bluetooth.bluez.internal.events.ServiceDataEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
@@ -358,6 +359,13 @@ public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEv
         }
     }
 
+    @Override
+    public void onServiceDataUpdate(ServiceDataEvent event) {
+        BluetoothScanNotification notification = new BluetoothScanNotification();
+        notification.setServiceData(event.getData());
+        notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
+    }
+
     @Override
     public void onTxPowerUpdate(TXPowerEvent event) {
         this.txPower = (int) event.getTxPower();
index 4d980eb0cf377594e1a22375cc41bffa4473ea82..88903068cad35445f3a3e1cdc8c66af4e9d796ba 100644 (file)
@@ -34,6 +34,7 @@ import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
+import org.openhab.binding.bluetooth.bluez.internal.events.ServiceDataEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
 import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
 import org.openhab.core.common.ThreadPoolManager;
@@ -46,6 +47,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Benjamin Lafois - Initial contribution and API
  * @author Connor Petty - Code cleanup
+ * @author Peter Rosenberg - Add support for ServiceData
  */
 @NonNullByDefault
 public class BlueZPropertiesChangedHandler extends AbstractPropertiesChangedHandler {
@@ -115,6 +117,9 @@ public class BlueZPropertiesChangedHandler extends AbstractPropertiesChangedHand
                     case "manufacturerdata":
                         onManufacturerDataUpdate(dbusPath, variant);
                         break;
+                    case "servicedata":
+                        onServiceDataUpdate(dbusPath, variant);
+                        break;
                     case "powered":
                         onPoweredUpdate(dbusPath, variant);
                         break;
@@ -196,6 +201,28 @@ public class BlueZPropertiesChangedHandler extends AbstractPropertiesChangedHand
         }
     }
 
+    private void onServiceDataUpdate(String dbusPath, Variant<?> variant) {
+        Map<String, byte[]> serviceData = new HashMap<>();
+
+        Object map = variant.getValue();
+        if (map instanceof DBusMap) {
+            DBusMap<?, ?> dbm = (DBusMap<?, ?>) map;
+            for (Map.Entry<?, ?> entry : dbm.entrySet()) {
+                Object key = entry.getKey();
+                Object value = entry.getValue();
+                if (key instanceof String && value instanceof Variant<?>) {
+                    value = ((Variant<?>) value).getValue();
+                    if (value instanceof byte[]) {
+                        serviceData.put(((String) key), ((byte[]) value));
+                    }
+                }
+            }
+        }
+        if (!serviceData.isEmpty()) {
+            notifyListeners(new ServiceDataEvent(dbusPath, serviceData));
+        }
+    }
+
     private void onValueUpdate(String dbusPath, Variant<?> variant) {
         Object value = variant.getValue();
         if (value instanceof byte[]) {
index 23670df77c0d28f4e5b1f9bdc10554b6801555cc..2a65baa6f2d1792ce12bded062dd79b15da571c6 100644 (file)
@@ -49,6 +49,10 @@ public interface BlueZEventListener {
         onDBusBlueZEvent(event);
     }
 
+    public default void onServiceDataUpdate(ServiceDataEvent event) {
+        onDBusBlueZEvent(event);
+    }
+
     public default void onConnectedStatusUpdate(ConnectedEvent event) {
         onDBusBlueZEvent(event);
     }
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServiceDataEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServiceDataEvent.java
new file mode 100644 (file)
index 0000000..6ab5b5f
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.bluetooth.bluez.internal.events;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * This event is triggered when an update to a device's service data is received.
+ *
+ * @author Peter Rosenberg - Initial Contribution
+ *
+ */
+@NonNullByDefault
+public class ServiceDataEvent extends BlueZEvent {
+
+    final private Map<String, byte[]> data;
+
+    public ServiceDataEvent(String dbusPath, Map<String, byte[]> data) {
+        super(dbusPath);
+        this.data = data;
+    }
+
+    public Map<String, byte[]> getData() {
+        return data;
+    }
+
+    @Override
+    public void dispatch(BlueZEventListener listener) {
+        listener.onServiceDataUpdate(this);
+    }
+}
index c68f01b755449f8867c3f2058149b641e2490549..644e23bf898f80dd1932a27f76c839eb85580508 100644 (file)
@@ -28,7 +28,9 @@ import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
+import org.openhab.binding.bluetooth.BluetoothService;
 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
+import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
 import org.openhab.bluetooth.gattparser.BluetoothGattParser;
 import org.openhab.bluetooth.gattparser.BluetoothGattParserFactory;
 import org.openhab.bluetooth.gattparser.FieldHolder;
@@ -57,7 +59,7 @@ import org.slf4j.LoggerFactory;
  * channels based off of a bluetooth device's GATT characteristics.
  *
  * @author Connor Petty - Initial contribution
- * @author Peter Rosenberg - Use notifications
+ * @author Peter Rosenberg - Use notifications, add support for ServiceData
  *
  */
 @NonNullByDefault
@@ -159,6 +161,71 @@ public class GenericBluetoothHandler extends ConnectedBluetoothHandler {
         getCharacteristicHandler(characteristic).handleCharacteristicUpdate(value);
     }
 
+    @Override
+    public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
+        super.onScanRecordReceived(scanNotification);
+
+        handleServiceData(scanNotification);
+    }
+
+    /**
+     * Service data is specified in the "Core Specification Supplement"
+     * https://www.bluetooth.com/specifications/specs/
+     * 1.11 SERVICE DATA
+     * <p>
+     * Broadcast configuration to configure what to advertise in service data
+     * is specified in "Core Specification 5.3"
+     * https://www.bluetooth.com/specifications/specs/
+     * Part G: GENERIC ATTRIBUTE PROFILE (GATT): 2.7 CONFIGURED BROADCAST
+     *
+     * This method extracts ServiceData, finds the Service and the Characteristic it belongs
+     * to and notifies a value change.
+     *
+     * @param scanNotification to get serviceData from
+     */
+    private void handleServiceData(BluetoothScanNotification scanNotification) {
+        Map<String, byte[]> serviceData = scanNotification.getServiceData();
+        if (serviceData != null) {
+            for (String uuidStr : serviceData.keySet()) {
+                @Nullable
+                BluetoothService service = device.getServices(UUID.fromString(uuidStr));
+                if (service == null) {
+                    logger.warn("Service with UUID {} not found on {}, ignored.", uuidStr,
+                            scanNotification.getAddress());
+                } else {
+                    // The ServiceData contains the UUID of the Service but no identifier of the
+                    // Characteristic the data belongs to.
+                    // Check which Characteristic within this service has the `Broadcast` property set
+                    // and select this one as the Characteristic to assign the data to.
+                    List<BluetoothCharacteristic> broadcastCharacteristics = service.getCharacteristics().stream()
+                            .filter((characteristic) -> characteristic
+                                    .hasPropertyEnabled(BluetoothCharacteristic.PROPERTY_BROADCAST))
+                            .collect(Collectors.toUnmodifiableList());
+
+                    if (broadcastCharacteristics.size() == 0) {
+                        logger.info(
+                                "No Characteristic of service with UUID {} on {} has the broadcast property set, ignored.",
+                                uuidStr, scanNotification.getAddress());
+                    } else if (broadcastCharacteristics.size() > 1) {
+                        logger.warn(
+                                "Multiple Characteristics of service with UUID {} on {} have the broadcast property set what is not supported, ignored.",
+                                uuidStr, scanNotification.getAddress());
+                    } else {
+                        BluetoothCharacteristic broadcastCharacteristic = broadcastCharacteristics.get(0);
+
+                        byte[] value = serviceData.get(uuidStr);
+                        if (value != null) {
+                            onCharacteristicUpdate(broadcastCharacteristic, value);
+                        } else {
+                            logger.warn("Service Data for Service with UUID {} on {} is null, ignored.", uuidStr,
+                                    scanNotification.getAddress());
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     private void updateThingChannels() {
         List<Channel> channels = device.getServices().stream()//
                 .flatMap(service -> service.getCharacteristics().stream())//
index ad3d9c9880ce03fa0ce03511d271c65c91d3e793..2d86f0c4da42d39201d441df65198a540b6c5f29 100644 (file)
@@ -224,6 +224,11 @@ public class BeaconBluetoothHandler extends BaseThingHandler implements Bluetoot
         int rssi = scanNotification.getRssi();
         if (rssi != Integer.MIN_VALUE) {
             updateRSSI(rssi);
+        } else {
+            // we received a scan notification from this device so it is online
+            // TODO how can we detect if the underlying bluez stack is still receiving advertising packets when there
+            // are no changes?
+            updateStatus(ThingStatus.ONLINE);
         }
     }
 
index fdddc6f64795f7e30499b264d70d3bb8958c36bc..36bbf6fc0f27f0cff32e4d66a3515280a0e25dc0 100644 (file)
@@ -85,11 +85,21 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
             idleDisconnectDelay = ((Number) idleDisconnectDelayRaw).intValue();
         }
 
-        if (alwaysConnected) {
+        // Start the recurrent job if the device is always connected
+        // or if the Services where not yet discovered.
+        // If the device is not always connected, the job will be terminated
+        // after successful connection and the device disconnected after Service
+        // discovery in `onServicesDiscovered()`.
+        if (alwaysConnected || !device.isServicesDiscovered()) {
             reconnectJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
                 try {
                     if (device.getConnectionState() != ConnectionState.CONNECTED) {
-                        if (!device.connect()) {
+                        if (device.connect()) {
+                            if (!alwaysConnected) {
+                                cancel(reconnectJob, false);
+                                reconnectJob = null;
+                            }
+                        } else {
                             logger.debug("Failed to connect to {}", address);
                         }
                         // we do not set the Thing status here, because we will anyhow receive a call to
@@ -326,4 +336,14 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
                     descriptor.getUuid(), address);
         }
     }
+
+    @Override
+    public void onServicesDiscovered() {
+        super.onServicesDiscovered();
+
+        if (!alwaysConnected && device.getConnectionState() == ConnectionState.CONNECTED) {
+            // disconnect when the device was only connected to discover the Services.
+            disconnect();
+        }
+    }
 }
index 9dd207ec96a7bc7adeeca16592b3954d819b56a4..0fb8adca5761e472270665f89805d71db1b35e8f 100644 (file)
  */
 package org.openhab.binding.bluetooth.notification;
 
+import java.util.Map;
+
 /**
  * The {@link BluetoothScanNotification} provides a notification of a received scan packet
  *
  * @author Chris Jackson - Initial contribution
+ * @author Peter Rosenberg - Add support for ServiceData
  */
 public class BluetoothScanNotification extends BluetoothNotification {
     /**
@@ -33,6 +36,13 @@ public class BluetoothScanNotification extends BluetoothNotification {
      */
     private byte[] manufacturerData = null;
 
+    /**
+     * The service data.
+     * Key: UUID of the service
+     * Value: Data of the characteristic
+     */
+    private Map<String, byte[]> serviceData = null;
+
     /**
      * The beacon type
      */
@@ -106,6 +116,14 @@ public class BluetoothScanNotification extends BluetoothNotification {
         return manufacturerData;
     }
 
+    public void setServiceData(Map<String, byte[]> serviceData) {
+        this.serviceData = serviceData;
+    }
+
+    public Map<String, byte[]> getServiceData() {
+        return serviceData;
+    }
+
     /**
      * Sets the beacon type for this packet
      *