[bluetooth.generic] Added support for generic bluetooth devices (#8775)

* Generic Bluetooth Binding Initial Contribution

Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Connor Petty
2020-11-23 01:43:44 -08:00
committed by GitHub
parent 0c30d90757
commit fb7fcd886d
26 changed files with 1808 additions and 142 deletions

View File

@@ -30,7 +30,6 @@ public class BluetoothBindingConstants {
public static final String BINDING_ID = "bluetooth";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CONNECTED = new ThingTypeUID(BINDING_ID, "connected");
public static final ThingTypeUID THING_TYPE_BEACON = new ThingTypeUID(BINDING_ID, "beacon");
// List of all Channel Type IDs
@@ -40,6 +39,7 @@ public class BluetoothBindingConstants {
public static final String PROPERTY_TXPOWER = "txpower";
public static final String PROPERTY_MAXCONNECTIONS = "maxconnections";
public static final String PROPERTY_SOFTWARE_VERSION = "softwareVersion";
public static final String CONFIGURATION_ADDRESS = "address";
public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";

View File

@@ -221,6 +221,48 @@ public class BluetoothCharacteristic {
return gattDescriptors.get(uuid);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + instance;
result = prime * result + ((service == null) ? 0 : service.hashCode());
result = prime * result + ((uuid == null) ? 0 : uuid.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BluetoothCharacteristic other = (BluetoothCharacteristic) obj;
if (instance != other.instance) {
return false;
}
if (service == null) {
if (other.service != null) {
return false;
}
} else if (!service.equals(other.service)) {
return false;
}
if (uuid == null) {
if (other.uuid != null) {
return false;
}
} else if (!uuid.equals(other.uuid)) {
return false;
}
return true;
}
/**
* Get the stored value for this characteristic.
*

View File

@@ -19,20 +19,12 @@ import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.DefaultLocation;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.util.HexUtils;
@@ -40,11 +32,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a handler for generic Bluetooth devices in connected mode, which at the same time can be used
* as a base implementation for more specific thing handlers.
* This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
@NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
@@ -67,11 +57,21 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
super.initialize();
connectionJob = scheduler.scheduleWithFixedDelay(() -> {
if (device.getConnectionState() != ConnectionState.CONNECTED) {
device.connect();
// we do not set the Thing status here, because we will anyhow receive a call to onConnectionStateChange
try {
if (device.getConnectionState() != ConnectionState.CONNECTED) {
device.connect();
// we do not set the Thing status here, because we will anyhow receive a call to
// onConnectionStateChange
} else {
// just in case it was already connected to begin with
updateStatus(ThingStatus.ONLINE);
if (!resolved && !device.discoverServices()) {
logger.debug("Error while discovering services");
}
}
} catch (RuntimeException ex) {
logger.warn("Unexpected error occurred", ex);
}
updateRSSI();
}, 0, 30, TimeUnit.SECONDS);
}
@@ -81,18 +81,7 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
connectionJob.cancel(true);
connectionJob = null;
}
scheduler.submit(() -> {
try {
deviceLock.lock();
if (device != null) {
device.removeListener(this);
device.disconnect();
device = null;
}
} finally {
deviceLock.unlock();
}
});
super.dispose();
}
@Override
@@ -167,12 +156,6 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
if (!resolved) {
resolved = true;
logger.debug("Service discovery completed for '{}'", address);
BluetoothCharacteristic characteristic = device
.getCharacteristic(GattCharacteristic.BATTERY_LEVEL.getUUID());
if (characteristic != null) {
activateChannel(characteristic, DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID());
logger.debug("Added GATT characteristic '{}'", characteristic.getGattCharacteristic().name());
}
}
}
@@ -180,13 +163,9 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
super.onCharacteristicReadComplete(characteristic, status);
if (status == BluetoothCompletionStatus.SUCCESS) {
if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
updateBatteryLevel(characteristic);
} else {
if (logger.isDebugEnabled()) {
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(),
address, HexUtils.bytesToHex(characteristic.getByteValue()));
}
if (logger.isDebugEnabled()) {
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
HexUtils.bytesToHex(characteristic.getByteValue()));
}
} else {
logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
@@ -210,9 +189,6 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
logger.debug("Recieved update {} to characteristic {} of device {}",
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
}
if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
updateBatteryLevel(characteristic);
}
}
@Override
@@ -223,41 +199,4 @@ public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
descriptor.getUuid(), address);
}
}
protected void updateBatteryLevel(BluetoothCharacteristic characteristic) {
// the byte has values from 0-255, which we need to map to 0-100
Double level = characteristic.getValue()[0] / 2.55;
updateState(characteristic.getGattCharacteristic().name(), new DecimalType(level.intValue()));
}
protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID,
@Nullable String name) {
if (characteristic != null) {
String channelId = name != null ? name : characteristic.getGattCharacteristic().name();
if (channelId == null) {
// use the type id as a fallback
channelId = channelTypeUID.getId();
}
if (getThing().getChannel(channelId) == null) {
// the channel does not exist yet, so let's add it
ThingBuilder updatedThing = editThing();
Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), "Number")
.withType(channelTypeUID).build();
updatedThing.withChannel(channel);
updateThing(updatedThing.build());
logger.debug("Added channel '{}' to Thing '{}'", channelId, getThing().getUID());
}
deviceCharacteristics.add(characteristic);
device.enableNotifications(characteristic);
if (isLinked(channelId)) {
device.readCharacteristic(characteristic);
}
} else {
logger.debug("Characteristic is null - not activating any channel.");
}
}
protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID) {
activateChannel(characteristic, channelTypeUID, null);
}
}

View File

@@ -91,4 +91,14 @@ public interface BluetoothDiscoveryParticipant {
BiConsumer<BluetoothAdapter, DiscoveryResult> publisher) {
// do nothing by default
}
/**
* Overriding this method allows discovery participants to dictate the order in which they should be evaluated
* relative to other discovery participants. Participants with a lower order value are evaluated first.
*
* @return the order of this participant, default 0
*/
public default int order() {
return 0;
}
}

View File

@@ -14,6 +14,7 @@ package org.openhab.binding.bluetooth.discovery.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -35,7 +36,6 @@ import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
@@ -44,6 +44,7 @@ import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,9 +87,12 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
@Override
public DiscoveryResult get() {
List<BluetoothDiscoveryParticipant> sortedParticipants = new ArrayList<>(participants);
sortedParticipants.sort(Comparator.comparing(BluetoothDiscoveryParticipant::order));
// first see if any of the participants that don't require a connection recognize this device
List<BluetoothDiscoveryParticipant> connectionParticipants = new ArrayList<>();
for (BluetoothDiscoveryParticipant participant : participants) {
for (BluetoothDiscoveryParticipant participant : sortedParticipants) {
if (participant.requiresConnection(device)) {
connectionParticipants.add(participant);
continue;
@@ -105,25 +109,23 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
// Since we couldn't find a result, lets try the connection based participants
DiscoveryResult result = null;
if (!connectionParticipants.isEmpty()) {
BluetoothAddress address = device.getAddress();
if (isAddressAvailable(address)) {
result = findConnectionResult(connectionParticipants);
// make sure to disconnect before letting go of the device
if (device.getConnectionState() == ConnectionState.CONNECTED) {
try {
if (!device.disconnect()) {
logger.debug("Failed to disconnect from device {}", address);
}
} catch (RuntimeException ex) {
logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
device.getAdapter().getUID(), ex);
BluetoothAddress address = device.getAddress();
if (isAddressAvailable(address)) {
result = findConnectionResult(connectionParticipants);
// make sure to disconnect before letting go of the device
if (device.getConnectionState() == ConnectionState.CONNECTED) {
try {
if (!device.disconnect()) {
logger.debug("Failed to disconnect from device {}", address);
}
} catch (RuntimeException ex) {
logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address,
device.getAdapter().getUID(), ex);
}
}
}
if (result == null) {
result = createDefaultResult(device);
result = createDefaultResult();
}
return result;
}
@@ -133,8 +135,8 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address));
}
private DiscoveryResult createDefaultResult(BluetoothDevice device) {
// We did not find a thing type for this device, so let's treat it as a generic one
private DiscoveryResult createDefaultResult() {
// We did not find a thing type for this device, so let's treat it as a generic beacon
String label = device.getName();
if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
label = "Bluetooth Device";
@@ -154,42 +156,51 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
label += " (" + manufacturer + ")";
}
ThingUID thingUID = new ThingUID(BluetoothBindingConstants.THING_TYPE_BEACON, device.getAdapter().getUID(),
device.getAddress().toString().toLowerCase().replace(":", ""));
ThingTypeUID thingTypeUID = BluetoothBindingConstants.THING_TYPE_BEACON;
ThingUID thingUID = new ThingUID(thingTypeUID, device.getAdapter().getUID(),
device.getAddress().toString().toLowerCase().replace(":", ""));
// Create the discovery result and add to the inbox
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS).withTTL(DISCOVERY_TTL)
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
}
// this is really just a special return type for `ensureConnected`
private static class ConnectionException extends Exception {
}
private void ensureConnected() throws ConnectionException, InterruptedException {
if (device.getConnectionState() != ConnectionState.CONNECTED) {
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
// something failed, so we abandon connection discovery
throw new ConnectionException();
}
if (!awaitConnection(10, TimeUnit.SECONDS)) {
logger.debug("Connection to device {} timed out", device.getAddress());
throw new ConnectionException();
}
if (!servicesDiscovered) {
device.discoverServices();
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
logger.debug("Service discovery for device {} timed out", device.getAddress());
// something failed, so we abandon connection discovery
throw new ConnectionException();
}
}
readDeviceInformationIfMissing();
logger.debug("Device information fetched from the device: {}", device);
}
}
private @Nullable DiscoveryResult findConnectionResult(List<BluetoothDiscoveryParticipant> connectionParticipants) {
try {
device.addListener(this);
for (BluetoothDiscoveryParticipant participant : connectionParticipants) {
// we call this every time just in case a participant somehow closes the connection
if (device.getConnectionState() != ConnectionState.CONNECTED) {
if (device.getConnectionState() != ConnectionState.CONNECTING && !device.connect()) {
logger.debug("Connection attempt failed to start for device {}", device.getAddress());
// something failed, so we abandon connection discovery
return null;
}
if (!awaitConnection(1, TimeUnit.SECONDS)) {
logger.debug("Connection to device {} timed out", device.getAddress());
return null;
}
if (!servicesDiscovered) {
device.discoverServices();
if (!awaitServiceDiscovery(10, TimeUnit.SECONDS)) {
logger.debug("Service discovery for device {} timed out", device.getAddress());
// something failed, so we abandon connection discovery
return null;
}
}
readDeviceInformationIfMissing();
logger.debug("Device information fetched from the device: {}", device);
}
ensureConnected();
try {
DiscoveryResult result = participant.createResult(device);
if (result != null) {
@@ -199,7 +210,7 @@ public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, Blu
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
}
}
} catch (InterruptedException e) {
} catch (InterruptedException | ConnectionException e) {
// do nothing
} finally {
device.removeListener(this);

View File

@@ -19,7 +19,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -39,7 +38,6 @@ public class BluetoothHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
static {
SUPPORTED_THING_TYPES_UIDS.add(BluetoothBindingConstants.THING_TYPE_BEACON);
SUPPORTED_THING_TYPES_UIDS.add(BluetoothBindingConstants.THING_TYPE_CONNECTED);
}
@Override
@@ -53,8 +51,6 @@ public class BluetoothHandlerFactory extends BaseThingHandlerFactory {
if (thingTypeUID.equals(BluetoothBindingConstants.THING_TYPE_BEACON)) {
return new BeaconBluetoothHandler(thing);
} else if (thingTypeUID.equals(BluetoothBindingConstants.THING_TYPE_CONNECTED)) {
return new ConnectedBluetoothHandler(thing);
}
return null;
}