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;
}
}
+ @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();
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;
*
* @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 {
case "manufacturerdata":
onManufacturerDataUpdate(dbusPath, variant);
break;
+ case "servicedata":
+ onServiceDataUpdate(dbusPath, variant);
+ break;
case "powered":
onPoweredUpdate(dbusPath, variant);
break;
}
}
+ 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[]) {
onDBusBlueZEvent(event);
}
+ public default void onServiceDataUpdate(ServiceDataEvent event) {
+ onDBusBlueZEvent(event);
+ }
+
public default void onConnectedStatusUpdate(ConnectedEvent event) {
onDBusBlueZEvent(event);
}
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
* 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
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())//
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);
}
}
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
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();
+ }
+ }
}
*/
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 {
/**
*/
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
*/
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
*