[bluetooth] Add support for service data (#10278)

Signed-off-by: Peter Rosenberg <prosenb.dev@gmail.com>
pull/13323/head
Pete 2022-08-26 05:36:21 +10:00 committed by GitHub
parent d4c472a04c
commit 34bdc21370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 195 additions and 3 deletions

View 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();

View 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[]) {

View 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);
}

View File

@ -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);
}
}

View 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())//

View 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);
}
}

View 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();
}
}
}

View File

@ -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<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
*