Skip to content

Commit

Permalink
[bluetooth] Add support for service data (openhab#10278)
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Rosenberg <[email protected]>
  • Loading branch information
PRosenb authored and leifbladt committed Oct 15, 2022
1 parent dafb536 commit 578c9c5
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -196,6 +201,28 @@ private void onManufacturerDataUpdate(String dbusPath, Variant<?> variant) {
}
}

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[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
* <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())//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -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
*/
Expand Down Expand Up @@ -106,6 +116,14 @@ public byte[] getManufacturerData() {
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
*
Expand Down

0 comments on commit 578c9c5

Please sign in to comment.