[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:
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user