diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java index 137aafbae250a..575dda68148d7 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java @@ -37,6 +37,7 @@ 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 void onManufacturerDataUpdate(ManufacturerDataEvent event) { } } + @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(); diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java index 4d980eb0cf377..88903068cad35 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java @@ -34,6 +34,7 @@ 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 @@ * * @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 void handle(@Nullable PropertiesChanged properties) { case "manufacturerdata": onManufacturerDataUpdate(dbusPath, variant); break; + case "servicedata": + onServiceDataUpdate(dbusPath, variant); + break; case "powered": onPoweredUpdate(dbusPath, variant); break; @@ -196,6 +201,28 @@ private void onManufacturerDataUpdate(String dbusPath, Variant variant) { } } + private void onServiceDataUpdate(String dbusPath, Variant variant) { + Map 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[]) { diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java index 23670df77c0d2..2a65baa6f2d17 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java @@ -49,6 +49,10 @@ public default void onManufacturerDataUpdate(ManufacturerDataEvent event) { 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 index 0000000000000..6ab5b5f37ea05 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServiceDataEvent.java @@ -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 data; + + public ServiceDataEvent(String dbusPath, Map data) { + super(dbusPath); + this.data = data; + } + + public Map getData() { + return data; + } + + @Override + public void dispatch(BlueZEventListener listener) { + listener.onServiceDataUpdate(this); + } +} diff --git a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java index c68f01b755449..644e23bf898f8 100644 --- a/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth.generic/src/main/java/org/openhab/binding/bluetooth/generic/internal/GenericBluetoothHandler.java @@ -28,7 +28,9 @@ 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 @@ * 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 void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[ 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 + *

+ * 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 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 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 channels = device.getServices().stream()// .flatMap(service -> service.getCharacteristics().stream())// diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java index ad3d9c9880ce0..2d86f0c4da42d 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java @@ -224,6 +224,11 @@ public void onScanRecordReceived(BluetoothScanNotification scanNotification) { 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); } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java index fdddc6f64795f..36bbf6fc0f27f 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/ConnectedBluetoothHandler.java @@ -85,11 +85,21 @@ public void initialize() { 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 void onDescriptorUpdate(BluetoothDescriptor descriptor, byte[] value) { 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(); + } + } } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/notification/BluetoothScanNotification.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/notification/BluetoothScanNotification.java index 9dd207ec96a7b..0fb8adca5761e 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/notification/BluetoothScanNotification.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/notification/BluetoothScanNotification.java @@ -12,10 +12,13 @@ */ 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 serviceData = null; + /** * The beacon type */ @@ -106,6 +116,14 @@ public byte[] getManufacturerData() { return manufacturerData; } + public void setServiceData(Map serviceData) { + this.serviceData = serviceData; + } + + public Map getServiceData() { + return serviceData; + } + /** * Sets the beacon type for this packet *