added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bluetooth-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-bluetooth" description="Bluetooth Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,231 @@
/**
* Copyright (c) 2010-2020 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;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is a abstract superclass for BluetoothAdapter implementations. This class takes care of inactive device cleanup
* as well as handling background and active discovery logic.
*
* Subclasses will primarily be responsible for device discovery
*
* @author Connor Petty - Initial contribution from refactored code
*/
@NonNullByDefault
public abstract class AbstractBluetoothBridgeHandler<BD extends BaseBluetoothDevice> extends BaseBridgeHandler
implements BluetoothAdapter {
private final Logger logger = LoggerFactory.getLogger(AbstractBluetoothBridgeHandler.class);
// Set of discovery listeners
private final Set<BluetoothDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
// Map of Bluetooth devices known to this bridge.
// This contains the devices from the most recent scan
private final Map<BluetoothAddress, BD> devices = new ConcurrentHashMap<>();
// Actual discovery status.
protected volatile boolean activeScanEnabled = false;
private BaseBluetoothBridgeHandlerConfiguration config = new BaseBluetoothBridgeHandlerConfiguration();
private @Nullable ScheduledFuture<?> inactiveRemovalJob;
/**
* Constructor
*
* @param bridge the bridge definition for this handler
*/
public AbstractBluetoothBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public ThingUID getUID() {
return getThing().getUID();
}
@Override
public @Nullable String getLocation() {
return getThing().getLocation();
}
@Override
public @Nullable String getLabel() {
return getThing().getLabel();
}
@Override
public void initialize() {
config = getConfigAs(BaseBluetoothBridgeHandlerConfiguration.class);
int intervalSecs = config.inactiveDeviceCleanupInterval;
inactiveRemovalJob = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, intervalSecs, intervalSecs,
TimeUnit.SECONDS);
}
@Override
public void dispose() {
ScheduledFuture<?> inactiveRemovalJob = this.inactiveRemovalJob;
if (inactiveRemovalJob != null) {
inactiveRemovalJob.cancel(true);
}
this.inactiveRemovalJob = null;
synchronized (devices) {
for (BD device : devices.values()) {
removeDevice(device);
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
private void removeInactiveDevices() {
// clean up orphaned entries
synchronized (devices) {
for (BD device : devices.values()) {
if (shouldRemove(device)) {
logger.debug("Removing device '{}' due to inactivity", device.getAddress());
removeDevice(device);
}
}
}
}
protected void removeDevice(BD device) {
device.dispose();
synchronized (devices) {
devices.remove(device.getAddress());
}
discoveryListeners.forEach(listener -> listener.deviceRemoved(device));
}
private boolean shouldRemove(BD device) {
// we can't remove devices with listeners since that means they have a handler.
if (device.hasListeners()) {
return false;
}
// devices that are connected won't receive any scan notifications so we can't remove them for being idle
if (device.getConnectionState() == ConnectionState.CONNECTED) {
return false;
}
ZonedDateTime lastActiveTime = device.getLastSeenTime();
if (lastActiveTime == null) {
// we want any new device to at least live a certain amount of time so it has a chance to be discovered or
// listened to.
lastActiveTime = device.createTime;
}
// we remove devices we haven't seen in a while
return ZonedDateTime.now().minusSeconds(config.inactiveDeviceCleanupThreshold).isAfter(lastActiveTime);
}
@Override
public void addDiscoveryListener(BluetoothDiscoveryListener listener) {
discoveryListeners.add(listener);
}
@Override
public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) {
discoveryListeners.remove(listener);
}
@Override
public void scanStart() {
// Enable scanning even while discovery is disabled in config. This allows manual starting discovery.
activeScanEnabled = true;
refreshDiscoveredDevices();
}
protected void refreshDiscoveredDevices() {
logger.debug("Refreshing Bluetooth device list...");
synchronized (devices) {
devices.values().forEach(this::deviceDiscovered);
}
}
@Override
public void scanStop() {
// Set active discovery state back to the configured discovery state.
activeScanEnabled = false;
// We need to keep the adapter in discovery mode as we otherwise won't get any RSSI updates either
}
@Override
public BD getDevice(BluetoothAddress address) {
synchronized (devices) {
return devices.computeIfAbsent(address, this::createDevice);
}
}
protected abstract BD createDevice(BluetoothAddress address);
@Override
public boolean hasHandlerForDevice(BluetoothAddress address) {
String addrStr = address.toString();
/*
* This type of search is inefficient and won't scale as the number of bluetooth Thing children increases on
* this bridge. But implementing a more efficient search would require a bit more overhead.
* Luckily though, it is reasonable to assume that the number of Thing children will remain small.
*/
for (Thing childThing : getThing().getThings()) {
Object childAddr = childThing.getConfiguration().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS);
if (addrStr.equals(childAddr)) {
return childThing.getHandler() != null;
}
}
return false;
}
public void deviceDiscovered(BluetoothDevice device) {
if (hasHandlerForDevice(device.getAddress())) {
// no point in discovering a device that already has a handler
return;
}
if (config.backgroundDiscovery || activeScanEnabled) {
if (deviceReachable(device)) {
discoveryListeners.forEach(listener -> listener.deviceDiscovered(device));
} else {
logger.trace("Not notifying listeners for device '{}', because it is not reachable.",
device.getAddress());
}
}
}
private boolean deviceReachable(BluetoothDevice device) {
Integer rssi = device.getRssi();
return rssi != null && rssi != 0;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This is the base configuration that all bluetooth bridge implementations will use.
* Bridges may choose to use a subclass of this class as their configuration in order to
* support more options.
*
* @author Connor Petty - Initial contribution from refactored code
*/
@NonNullByDefault
public class BaseBluetoothBridgeHandlerConfiguration {
public boolean backgroundDiscovery = false;
public int inactiveDeviceCleanupInterval = 60;
public int inactiveDeviceCleanupThreshold = 300;
}

View File

@@ -0,0 +1,295 @@
/**
* Copyright (c) 2010-2020 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;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BaseBluetoothDevice} implements parts of the BluetoothDevice functionality that is
* shared to all concrete BluetoothDevice implementations.
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public abstract class BaseBluetoothDevice extends BluetoothDevice {
private final Logger logger = LoggerFactory.getLogger(BaseBluetoothDevice.class);
/**
* Current connection state
*/
protected ConnectionState connectionState = ConnectionState.DISCOVERING;
/**
* Manufacturer id
*/
protected @Nullable Integer manufacturer = null;
/**
* Device name.
* <p>
* Uses the devices long name if known, otherwise the short name if known
*/
protected @Nullable String name;
/**
* List of supported services
*/
protected final Map<UUID, BluetoothService> supportedServices = new ConcurrentHashMap<>();
/**
* Last known RSSI
*/
protected @Nullable Integer rssi = null;
/**
* Last reported transmitter power
*/
protected @Nullable Integer txPower = null;
protected final transient ZonedDateTime createTime = ZonedDateTime.now();
/**
* Last time when activity occurred on this device.
*/
protected @Nullable ZonedDateTime lastSeenTime = null;
/**
* The event listeners will be notified of device updates
*/
private final Set<BluetoothDeviceListener> eventListeners = new CopyOnWriteArraySet<>();
/**
* Construct a Bluetooth device taking the Bluetooth address
*
* @param adapter
* @param sender
*/
public BaseBluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
super(adapter, address);
}
/**
* Returns the last time this device was active
*
* @return The last time this device was active
*/
public @Nullable ZonedDateTime getLastSeenTime() {
return lastSeenTime;
}
/**
* Updates the last activity timestamp for this device.
* Should be called whenever activity occurs on this device.
*
*/
public void updateLastSeenTime() {
lastSeenTime = ZonedDateTime.now();
}
/**
* Returns the name of the Bluetooth device.
*
* @return The devices name
*/
@Override
public @Nullable String getName() {
return name;
}
/**
* Sets the manufacturer id for the device
*
* @param manufacturer the manufacturer id
*/
public void setManufacturerId(int manufacturer) {
this.manufacturer = manufacturer;
}
/**
* Returns the manufacturer ID of the device
*
* @return an integer with manufacturer ID of the device, or null if not known
*/
@Override
public @Nullable Integer getManufacturerId() {
return manufacturer;
}
/**
* Returns a {@link BluetoothService} if the requested service is supported
*
* @return the {@link BluetoothService} or null if the service is not supported.
*/
@Override
public @Nullable BluetoothService getServices(UUID uuid) {
return supportedServices.get(uuid);
}
/**
* Returns a list of supported service UUIDs
*
* @return list of supported {@link BluetoothService}s.
*/
@Override
public Collection<BluetoothService> getServices() {
return supportedServices.values();
}
/**
* Sets the device transmit power
*
* @param power the current transmitter power in dBm
*/
public void setTxPower(int txPower) {
this.txPower = txPower;
}
/**
* Returns the last Transmit Power value or null if no transmit power has been received
*
* @return the last reported transmitter power value in dBm
*/
@Override
public @Nullable Integer getTxPower() {
return txPower;
}
/**
* Sets the current Receive Signal Strength Indicator (RSSI) value
*
* @param rssi the current RSSI value in dBm
* @return true if the RSSI has changed, false if it was the same as previous
*/
public boolean setRssi(int rssi) {
boolean changed = (this.rssi == null || this.rssi != rssi);
this.rssi = rssi;
return changed;
}
/**
* Returns the last Receive Signal Strength Indicator (RSSI) value or null if no RSSI has been received
*
* @return the last RSSI value in dBm
*/
@Override
public @Nullable Integer getRssi() {
return rssi;
}
/**
* Set the name of the device
*
* @param name a {@link String} defining the device name
*/
public void setName(String name) {
this.name = name;
}
/**
* Check if the device supports the specified service
*
* @param uuid the service {@link UUID}
* @return true if the service is supported
*/
@Override
public boolean supportsService(UUID uuid) {
return supportedServices.containsKey(uuid);
}
/**
* Get the current connection state for this device
*
* @return the current {@link ConnectionState}
*/
@Override
public ConnectionState getConnectionState() {
return connectionState;
}
/**
* Adds a service to the device.
*
* @param service the new {@link BluetoothService} to add
* @return true if the service was added or false if the service was already supported
*/
@Override
protected boolean addService(BluetoothService service) {
BluetoothService oldValue = supportedServices.putIfAbsent(service.getUuid(), service);
if (oldValue == null) {
logger.trace("Adding new service to device {}: {}", address, service);
return true;
}
return false;
}
@Override
protected Collection<BluetoothDeviceListener> getListeners() {
return eventListeners;
}
/**
* Releases resources that this device is using.
*
*/
@Override
protected void dispose() {
}
@Override
protected void notifyListeners(BluetoothEventType event, Object... args) {
switch (event) {
case SCAN_RECORD:
case CHARACTERISTIC_UPDATED:
case DESCRIPTOR_UPDATED:
case SERVICES_DISCOVERED:
updateLastSeenTime();
break;
default:
break;
}
super.notifyListeners(event, args);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("BluetoothDevice [address=");
builder.append(address);
builder.append(", manufacturer=");
builder.append(manufacturer);
if (BluetoothCompanyIdentifiers.get(manufacturer) != null) {
builder.append(" (");
builder.append(BluetoothCompanyIdentifiers.get(manufacturer));
builder.append(')');
}
builder.append(", name=");
builder.append(name);
builder.append(", rssi=");
builder.append(rssi);
builder.append(']');
return builder.toString();
}
}

View File

@@ -0,0 +1,268 @@
/**
* Copyright (c) 2010-2020 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;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import javax.measure.quantity.Power;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
/**
* This is a handler for generic Bluetooth devices in beacon-mode (i.e. not connected), which at the same time can be
* used as a base implementation for more specific thing handlers.
*
* @author Kai Kreuzer - Initial contribution and API
*/
@NonNullByDefault
public class BeaconBluetoothHandler extends BaseThingHandler implements BluetoothDeviceListener {
@NonNullByDefault({} /* non-null if initialized */)
protected BluetoothAdapter adapter;
@NonNullByDefault({} /* non-null if initialized */)
protected BluetoothAddress address;
@NonNullByDefault({} /* non-null if initialized */)
protected BluetoothDevice device;
protected final ReentrantLock deviceLock;
private @Nullable ZonedDateTime lastActivityTime;
public BeaconBluetoothHandler(Thing thing) {
super(thing);
deviceLock = new ReentrantLock();
}
@Override
public void initialize() {
try {
address = new BluetoothAddress(getConfig().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS).toString());
} catch (IllegalArgumentException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
return;
}
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Not associated with any bridge");
return;
}
BridgeHandler bridgeHandler = bridge.getHandler();
if (!(bridgeHandler instanceof BluetoothAdapter)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Associated with an unsupported bridge");
return;
}
adapter = (BluetoothAdapter) bridgeHandler;
try {
deviceLock.lock();
device = adapter.getDevice(address);
device.addListener(this);
} finally {
deviceLock.unlock();
}
ThingBuilder builder = editThing();
for (Channel channel : createDynamicChannels()) {
// we only want to add each channel, not replace all of them
builder.withChannel(channel);
}
updateThing(builder.build());
updateStatus(ThingStatus.UNKNOWN);
}
private Channel buildChannel(String channelType, String itemType) {
return ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelType), itemType).build();
}
protected List<Channel> createDynamicChannels() {
List<Channel> channels = new ArrayList<>();
channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, "Number:Power"));
if (device instanceof DelegateBluetoothDevice) {
channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, "String"));
channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, "String"));
}
return channels;
}
@Override
public void dispose() {
try {
deviceLock.lock();
if (device != null) {
device.removeListener(this);
device.disconnect();
device = null;
}
} finally {
deviceLock.unlock();
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
switch (channelUID.getId()) {
case BluetoothBindingConstants.CHANNEL_TYPE_RSSI:
updateRSSI();
break;
case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER:
updateAdapter();
break;
case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION:
updateAdapterLocation();
break;
}
}
}
/**
* Updates the RSSI channel and the Thing status according to the new received rssi value
*/
protected void updateRSSI() {
if (device != null) {
updateRSSI(device.getRssi());
}
}
private void updateRSSI(@Nullable Integer rssi) {
if (rssi != null && rssi != 0) {
QuantityType<Power> quantity = new QuantityType<>(rssi, SmartHomeUnits.DECIBEL_MILLIWATTS);
updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, quantity);
updateStatusBasedOnRssi(true);
} else {
updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, UnDefType.NULL);
updateStatusBasedOnRssi(false);
}
}
protected void updateAdapter() {
if (device != null) {
BluetoothAdapter adapter = device.getAdapter();
updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, new StringType(adapter.getUID().getId()));
}
}
protected void updateAdapterLocation() {
if (device != null) {
BluetoothAdapter adapter = device.getAdapter();
String location = adapter.getLocation();
if (location != null || StringUtils.isBlank(location)) {
updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, new StringType(location));
} else {
updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, UnDefType.NULL);
}
}
}
/**
* This method sets the Thing status based on whether or not we can receive a signal from it.
* This is the best logic for beacons, but connected devices might want to deactivate this by overriding the method.
*
* @param receivedSignal true, if the device is in reach
*/
protected void updateStatusBasedOnRssi(boolean receivedSignal) {
if (receivedSignal) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
private void onActivity() {
this.lastActivityTime = ZonedDateTime.now();
}
@Override
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
onActivity();
int rssi = scanNotification.getRssi();
if (rssi != Integer.MIN_VALUE) {
updateRSSI(rssi);
}
}
@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
// a disconnection doesn't count as activity
if (connectionNotification.getConnectionState() != ConnectionState.DISCONNECTED) {
onActivity();
}
}
@Override
public void onServicesDiscovered() {
onActivity();
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
if (status == BluetoothCompletionStatus.SUCCESS) {
onActivity();
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
if (status == BluetoothCompletionStatus.SUCCESS) {
onActivity();
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
onActivity();
}
@Override
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
onActivity();
}
@Override
public void onAdapterChanged(BluetoothAdapter adapter) {
updateAdapter();
updateAdapterLocation();
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.registry.Identifiable;
import org.openhab.core.thing.ThingUID;
/**
* The {@link BluetoothAdapter} class defines the standard adapter API that must be implemented by bridge handlers,
* which are then required to be registered as an BluetoothAdapter OSGi service.
* <p>
* <b>Scanning</b>
* The API assumes that the adapter is "always" scanning to enable beacons to be received.
* The bridge must decide to enable and disable scanning as it needs. This design choice avoids interaction between
* higher layers where a binding may want to enable scanning while another needs to disable scanning for a specific
* function (e.g. to connect to a device). The bridge should disable scanning only for the period that is needed.
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - renamed it, made it identifiable and added listener support
*/
@NonNullByDefault
public interface BluetoothAdapter extends Identifiable<ThingUID> {
/**
* Adds a {@link BluetoothDiscoveryListener} to the adapter
*
* @param listener the listener to add
*/
void addDiscoveryListener(BluetoothDiscoveryListener listener);
/**
* Removes a {@link BluetoothDiscoveryListener} from the adapter
*
* @param listener the listener to remove
*/
void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener);
/**
* Starts an active scan on the Bluetooth interface.
*/
void scanStart();
/**
* Stops an active scan on the Bluetooth interface
*/
void scanStop();
/**
* Gets the {@link BluetoothAddress} of the adapter
*
* @return the {@link BluetoothAddress} of the adapter
* @throws IllegalStateException if the adapter is not initialized
*/
@Nullable
BluetoothAddress getAddress();
/**
* Gets the {@link BluetoothDevice} given the {@link BluetoothAddress}.
* A {@link BluetoothDevice} will always be returned for a valid hardware address, even if this adapter has never
* seen that device.
*
* @param address the {@link BluetoothAddress} to retrieve
* @return the {@link BluetoothDevice}
*/
BluetoothDevice getDevice(BluetoothAddress address);
/**
* Gets the location of this adapter, as specified in Thing.getLocation()
*
* @return the location of this adapter
*/
@Nullable
String getLocation();
/**
* Gets the label for this adapter, as specified in Thing.getLabel()
*
* @return the location of this adapter
*/
@Nullable
String getLabel();
/**
* Checks if this adapter has a device with the given {@link BluetoothAddress}.
*
* @param address the {@link BluetoothAddress} to check for
* @return true if this adapter has a {@link BluetoothDevice} with that address
*/
boolean hasHandlerForDevice(BluetoothAddress address);
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 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;
/**
* The {@link BluetoothAddress} class defines a bluetooth address
*
* @author Chris Jackson - Initial contribution
*/
public class BluetoothAddress {
public static final int BD_ADDRESS_LENGTH = 17;
private final String address;
/**
* The default constructor
*
* @param address the device address
*/
public BluetoothAddress(String address) {
if (address == null || address.length() != BD_ADDRESS_LENGTH) {
throw new IllegalArgumentException("BT Address cannot be null and must be in format XX:XX:XX:XX:XX:XX");
}
for (int i = 0; i < BD_ADDRESS_LENGTH; i++) {
char c = address.charAt(i);
// Check address - 2 bytes should be hex, and then a colon
switch (i % 3) {
case 0: // fall through
case 1:
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) {
break;
}
throw new IllegalArgumentException("BT Address must contain upper case hex values only");
case 2:
if (c == ':') {
break;
}
throw new IllegalArgumentException("BT Address bytes must be separated with colon");
}
}
this.address = address;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((address == null) ? 0 : address.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;
}
BluetoothAddress other = (BluetoothAddress) obj;
if (address == null) {
if (other.address != null) {
return false;
}
} else if (!address.equals(other.address)) {
return false;
}
return true;
}
@Override
public String toString() {
return address;
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BluetoothBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - refactoring and extension
*/
@NonNullByDefault
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
public static final String CHANNEL_TYPE_RSSI = "rssi";
public static final String CHANNEL_TYPE_ADAPTER = "adapterUID";
public static final String CHANNEL_TYPE_ADAPTER_LOCATION = "adapterLocation";
public static final String PROPERTY_TXPOWER = "txpower";
public static final String PROPERTY_MAXCONNECTIONS = "maxconnections";
public static final String CONFIGURATION_ADDRESS = "address";
public static final String CONFIGURATION_DISCOVERY = "backgroundDiscovery";
public static final long BLUETOOTH_BASE_UUID = 0x800000805f9b34fbL;
// Bluetooth profile UUID definitions
public static final UUID PROFILE_GATT = UUID.fromString("00001801-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_A2DP_SOURCE = UUID.fromString("0000110a-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_A2DP_SINK = UUID.fromString("0000110b-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_A2DP = UUID.fromString("0000110d-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_AVRCP_REMOTE = UUID.fromString("0000110c-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_CORDLESS_TELEPHONE = UUID.fromString("00001109-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_DID_PNPINFO = UUID.fromString("00001200-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_HEADSET = UUID.fromString("00001108-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_HFP = UUID.fromString("0000111e-0000-1000-8000-00805f9b34fb");
public static final UUID PROFILE_HFP_AUDIOGATEWAY = UUID.fromString("0000111f-0000-1000-8000-00805f9b34fb");
}

View File

@@ -0,0 +1,657 @@
/**
* Copyright (c) 2010-2020 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;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BluetoothCharacteristic} class defines the Bluetooth characteristic.
* <p>
* Characteristics are defined attribute types that contain a single logical value.
* <p>
* https://www.bluetooth.com/specifications/gatt/characteristics
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Cleaned up code
*/
public class BluetoothCharacteristic {
public static final int FORMAT_UINT8 = 0x11;
public static final int FORMAT_UINT16 = 0x12;
public static final int FORMAT_UINT32 = 0x14;
public static final int FORMAT_SINT8 = 0x21;
public static final int FORMAT_SINT16 = 0x22;
public static final int FORMAT_SINT32 = 0x24;
public static final int FORMAT_SFLOAT = 0x32;
public static final int FORMAT_FLOAT = 0x34;
public static final int PROPERTY_BROADCAST = 0x01;
public static final int PROPERTY_READ = 0x02;
public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
public static final int PROPERTY_WRITE = 0x08;
public static final int PROPERTY_NOTIFY = 0x10;
public static final int PROPERTY_INDICATE = 0x20;
public static final int PROPERTY_SIGNED_WRITE = 0x40;
public static final int PROPERTY_EXTENDED_PROPS = 0x80;
public static final int PERMISSION_READ = 0x01;
public static final int PERMISSION_READ_ENCRYPTED = 0x02;
public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;
public static final int PERMISSION_WRITE = 0x10;
public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;
public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;
public static final int PERMISSION_WRITE_SIGNED = 0x80;
public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;
public static final int WRITE_TYPE_DEFAULT = 0x02;
public static final int WRITE_TYPE_NO_RESPONSE = 0x01;
public static final int WRITE_TYPE_SIGNED = 0x04;
private final Logger logger = LoggerFactory.getLogger(BluetoothCharacteristic.class);
/**
* The {@link UUID} for this characteristic
*/
protected UUID uuid;
/**
* The handle for this characteristic
*/
protected int handle;
/**
* A map of {@link BluetoothDescriptor}s applicable to this characteristic
*/
protected Map<UUID, BluetoothDescriptor> gattDescriptors = new HashMap<>();
protected int instance;
protected int properties;
protected int permissions;
protected int writeType;
/**
* The raw data value for this characteristic
*/
protected int[] value = new int[0];
/**
* The {@link BluetoothService} to which this characteristic belongs
*/
protected BluetoothService service;
/**
* Create a new BluetoothCharacteristic.
*
* @param uuid the {@link UUID} of the new characteristic
* @param handle
*/
public BluetoothCharacteristic(UUID uuid, int handle) {
this.uuid = uuid;
this.handle = handle;
}
/**
* Adds a descriptor to this characteristic.
*
* @param descriptor {@link BluetoothDescriptor} to be added to this characteristic.
* @return true, if the descriptor was added to the characteristic
*/
public boolean addDescriptor(BluetoothDescriptor descriptor) {
if (gattDescriptors.get(descriptor.getUuid()) != null) {
return false;
}
gattDescriptors.put(descriptor.getUuid(), descriptor);
return true;
}
/**
* Returns the {@link UUID} of this characteristic
*
* @return UUID of this characteristic
*/
public UUID getUuid() {
return uuid;
}
/**
* Returns the instance ID for this characteristic.
*
* If a remote device offers multiple characteristics with the same UUID, the instance ID is used to distinguish
* between characteristics.
*
* @return Instance ID of this characteristic
*/
public int getInstanceId() {
return instance;
}
/**
* Returns the properties of this characteristic.
*
* The properties contain a bit mask of property flags indicating the features of this characteristic.
*
*/
public int getProperties() {
return properties;
}
/**
* Returns the permissions for this characteristic.
*/
public int getPermissions() {
return permissions;
}
/**
* Gets the write type for this characteristic.
*
*/
public int getWriteType() {
return writeType;
}
/**
* Set the write type for this characteristic
*
* @param writeType
*/
public void setWriteType(int writeType) {
this.writeType = writeType;
}
/**
* Get the service to which this characteristic belongs
*
* @return the {@link BluetoothService}
*/
public BluetoothService getService() {
return service;
}
/**
* Returns the handle for this characteristic
*
* @return the handle for the characteristic
*/
public int getHandle() {
return handle;
}
/**
* Get the service to which this characteristic belongs
*
* @return the {@link BluetoothService}
*/
public void setService(BluetoothService service) {
this.service = service;
}
/**
* Returns a list of descriptors for this characteristic.
*
*/
public List<BluetoothDescriptor> getDescriptors() {
return new ArrayList<>(gattDescriptors.values());
}
/**
* Returns a descriptor with a given UUID out of the list of
* descriptors for this characteristic.
*
* @return the {@link BluetoothDescriptor}
*/
public BluetoothDescriptor getDescriptor(UUID uuid) {
return gattDescriptors.get(uuid);
}
/**
* Get the stored value for this characteristic.
*
*/
public int[] getValue() {
return value;
}
/**
* Get the stored value for this characteristic.
*
*/
public byte[] getByteValue() {
byte[] byteValue = new byte[value.length];
for (int cnt = 0; cnt < value.length; cnt++) {
byteValue[cnt] = (byte) (value[cnt] & 0xFF);
}
return byteValue;
}
/**
* Return the stored value of this characteristic.
*
*/
public Integer getIntValue(int formatType, int offset) {
if ((offset + getTypeLen(formatType)) > value.length) {
return null;
}
switch (formatType) {
case FORMAT_UINT8:
return unsignedByteToInt(value[offset]);
case FORMAT_UINT16:
return unsignedBytesToInt(value[offset], value[offset + 1]);
case FORMAT_UINT32:
return unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
case FORMAT_SINT8:
return unsignedToSigned(unsignedByteToInt(value[offset]), 8);
case FORMAT_SINT16:
return unsignedToSigned(unsignedBytesToInt(value[offset], value[offset + 1]), 16);
case FORMAT_SINT32:
return unsignedToSigned(
unsignedBytesToInt(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]), 32);
default:
logger.error("Unknown format type {} - no int value can be provided for it.", formatType);
}
return null;
}
/**
* Return the stored value of this characteristic. This doesn't read the remote data.
*
*/
public Float getFloatValue(int formatType, int offset) {
if ((offset + getTypeLen(formatType)) > value.length) {
return null;
}
switch (formatType) {
case FORMAT_SFLOAT:
return bytesToFloat(value[offset], value[offset + 1]);
case FORMAT_FLOAT:
return bytesToFloat(value[offset], value[offset + 1], value[offset + 2], value[offset + 3]);
default:
logger.error("Unknown format type {} - no float value can be provided for it.", formatType);
}
return null;
}
/**
* Return the stored value of this characteristic. This doesn't read the remote data.
*
*/
public String getStringValue(int offset) {
if (value == null || offset > value.length) {
return null;
}
byte[] strBytes = new byte[value.length - offset];
for (int i = 0; i < (value.length - offset); ++i) {
strBytes[i] = (byte) value[offset + i];
}
return new String(strBytes, StandardCharsets.UTF_8);
}
/**
* Updates the locally stored value of this characteristic.
*
* @param value the value to set
* @return true, if it has been set successfully
*/
public boolean setValue(int[] value) {
this.value = value;
return true;
}
/**
* Set the local value of this characteristic.
*
* @param value the value to set
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
* @param offset the offset to use when interpreting the value
* @return true, if it has been set successfully
*/
public boolean setValue(int value, int formatType, int offset) {
int len = offset + getTypeLen(formatType);
if (this.value == null) {
this.value = new int[len];
}
if (len > this.value.length) {
return false;
}
int val = value;
switch (formatType) {
case FORMAT_SINT8:
val = intToSignedBits(value, 8);
// Fall-through intended
case FORMAT_UINT8:
this.value[offset] = (byte) (val & 0xFF);
break;
case FORMAT_SINT16:
val = intToSignedBits(value, 16);
// Fall-through intended
case FORMAT_UINT16:
this.value[offset] = (byte) (val & 0xFF);
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
break;
case FORMAT_SINT32:
val = intToSignedBits(value, 32);
// Fall-through intended
case FORMAT_UINT32:
this.value[offset] = (byte) (val & 0xFF);
this.value[offset + 1] = (byte) ((val >> 8) & 0xFF);
this.value[offset + 2] = (byte) ((val >> 16) & 0xFF);
this.value[offset + 2] = (byte) ((val >> 24) & 0xFF);
break;
default:
return false;
}
return true;
}
/**
* Set the local value of this characteristic.
*
* @param mantissa the mantissa of the value
* @param exponent the exponent of the value
* @param formatType the format of the value (as one of the FORMAT_* constants in this class)
* @param offset the offset to use when interpreting the value
* @return true, if it has been set successfully
*
*/
public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
int len = offset + getTypeLen(formatType);
if (value == null) {
value = new int[len];
}
if (len > value.length) {
return false;
}
switch (formatType) {
case FORMAT_SFLOAT:
int m = intToSignedBits(mantissa, 12);
int exp = intToSignedBits(exponent, 4);
value[offset] = (byte) (m & 0xFF);
value[offset + 1] = (byte) ((m >> 8) & 0x0F);
value[offset + 1] += (byte) ((exp & 0x0F) << 4);
break;
case FORMAT_FLOAT:
m = intToSignedBits(mantissa, 24);
exp = intToSignedBits(exponent, 8);
value[offset] = (byte) (m & 0xFF);
value[offset + 1] = (byte) ((m >> 8) & 0xFF);
value[offset + 2] = (byte) ((m >> 16) & 0xFF);
value[offset + 2] += (byte) (exp & 0xFF);
break;
default:
return false;
}
return true;
}
/**
* Set the local value of this characteristic.
*
* @param value the value to set
* @return true, if it has been set successfully
*/
public boolean setValue(byte[] value) {
this.value = new int[value.length];
int cnt = 0;
for (byte val : value) {
this.value[cnt++] = val;
}
return true;
}
/**
* Set the local value of this characteristic.
*
* @param value the value to set
* @return true, if it has been set successfully
*/
public boolean setValue(String value) {
this.value = new int[value.getBytes().length];
int cnt = 0;
for (byte val : value.getBytes()) {
this.value[cnt++] = val;
}
return true;
}
/**
* Returns the size of the requested value type.
*/
private int getTypeLen(int formatType) {
return formatType & 0xF;
}
/**
* Convert a signed byte to an unsigned int.
*/
private int unsignedByteToInt(int value) {
return value & 0xFF;
}
/**
* Convert signed bytes to a 16-bit unsigned int.
*/
private int unsignedBytesToInt(int value1, int value2) {
return value1 + (value2 << 8);
}
/**
* Convert signed bytes to a 32-bit unsigned int.
*/
private int unsignedBytesToInt(int value1, int value2, int value3, int value4) {
return value1 + (value2 << 8) + (value3 << 16) + (value4 << 24);
}
/**
* Convert signed bytes to a 16-bit short float value.
*/
private float bytesToFloat(int value1, int value2) {
int mantissa = unsignedToSigned(unsignedByteToInt(value1) + ((unsignedByteToInt(value2) & 0x0F) << 8), 12);
int exponent = unsignedToSigned(unsignedByteToInt(value2) >> 4, 4);
return (float) (mantissa * Math.pow(10, exponent));
}
/**
* Convert signed bytes to a 32-bit short float value.
*/
private float bytesToFloat(int value1, int value2, int value3, int value4) {
int mantissa = unsignedToSigned(
unsignedByteToInt(value1) + (unsignedByteToInt(value2) << 8) + (unsignedByteToInt(value3) << 16), 24);
return (float) (mantissa * Math.pow(10, value4));
}
/**
* Convert an unsigned integer to a two's-complement signed value.
*/
private int unsignedToSigned(int unsigned, int size) {
if ((unsigned & (1 << size - 1)) != 0) {
return -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
} else {
return unsigned;
}
}
/**
* Convert an integer into the signed bits of the specified length.
*/
private int intToSignedBits(int i, int size) {
if (i < 0) {
return (1 << size - 1) + (i & ((1 << size - 1) - 1));
} else {
return i;
}
}
public GattCharacteristic getGattCharacteristic() {
return GattCharacteristic.getCharacteristic(uuid);
}
public enum GattCharacteristic {
// Characteristic
ALERT_CATEGORY_ID(0x2A43),
ALERT_CATEGORY_ID_BIT_MASK(0x2A42),
ALERT_LEVEL(0x2A06),
ALERT_NOTIFICATION_CONTROL_POINT(0x2A44),
ALERT_STATUS(0x2A3F),
APPEARANCE(0x2A01),
BATTERY_LEVEL(0x2A19),
BLOOD_PRESSURE_FEATURE(0x2A49),
BLOOD_PRESSURE_MEASUREMENT(0x2A35),
BODY_SENSOR_LOCATION(0x2A38),
BOOT_KEYOBARD_INPUT_REPORT(0x2A22),
BOOT_KEYOBARD_OUTPUT_REPORT(0x2A32),
BOOT_MOUSE_INPUT_REPORT(0x2A33),
CSC_FEATURE(0x2A5C),
CSC_MEASUREMENT(0x2A5B),
CURRENT_TIME(0x2A2B),
CYCLING_POWER_CONTROL_POINT(0x2A66),
CYCLING_POWER_FEATURE(0x2A65),
CYCLING_POWER_MEASUREMENT(0x2A63),
CYCLING_POWER_VECTOR(0x2A64),
DATE_TIME(0x2A08),
DAY_DATE_TIME(0x2A0A),
DAY_OF_WEEK(0x2A09),
DEVICE_NAME(0x2A00),
DST_OFFSET(0x2A0D),
EXACT_TIME_256(0x2A0C),
FIRMWARE_REVISION_STRING(0x2A26),
GLUCOSE_FEATURE(0x2A51),
GLUCOSE_MEASUREMENT(0x2A18),
GLUCOSE_MEASUREMENT_CONTROL(0x2A34),
HARDWARE_REVISION_STRING(0x2A27),
HEART_RATE_CONTROL_POINT(0x2A39),
HEART_RATE_MEASUREMENT(0x2A37),
HID_CONTROL_POINT(0x2A4C),
HID_INFORMATION(0x2A4A),
IEEE11073_20601_REGULATORY_CERTIFICATION_DATA_LIST(0x2A2A),
INTERMEDIATE_CUFF_PRESSURE(0x2A36),
INTERMEDIATE_TEMPERATURE(0x2A1E),
LN_CONTROL_POINT(0x2A6B),
LN_FEATURE(0x2A6A),
LOCAL_TIME_INFORMATION(0x2A0F),
LOCATION_AND_SPEED(0x2A67),
MANUFACTURER_NAME_STRING(0x2A29),
MEASUREMENT_INTERVAL(0x2A21),
MODEL_NUMBER_STRING(0x2A24),
NAVIGATION(0x2A68),
NEW_ALERT(0x2A46),
PERIPERAL_PREFFERED_CONNECTION_PARAMETERS(0x2A04),
PERIPHERAL_PRIVACY_FLAG(0x2A02),
PN_PID(0x2A50),
POSITION_QUALITY(0x2A69),
PROTOCOL_MODE(0x2A4E),
RECONNECTION_ADDRESS(0x2A03),
RECORD_ACCESS_CONTROL_POINT(0x2A52),
REFERENCE_TIME_INFORMATION(0x2A14),
REPORT(0x2A4D),
REPORT_MAP(0x2A4B),
RINGER_CONTROL_POINT(0x2A40),
RINGER_SETTING(0x2A41),
RSC_FEATURE(0x2A54),
RSC_MEASUREMENT(0x2A53),
SC_CONTROL_POINT(0x2A55),
SCAN_INTERVAL_WINDOW(0x2A4F),
SCAN_REFRESH(0x2A31),
SENSOR_LOCATION(0x2A5D),
SERIAL_NUMBER_STRING(0x2A25),
SERVICE_CHANGED(0x2A05),
SOFTWARE_REVISION_STRING(0x2A28),
SUPPORTED_NEW_ALERT_CATEGORY(0x2A47),
SUPPORTED_UNREAD_ALERT_CATEGORY(0x2A48),
SYSTEM_ID(0x2A23),
TEMPERATURE_MEASUREMENT(0x2A1C),
TEMPERATURE_TYPE(0x2A1D),
TIME_ACCURACY(0x2A12),
TIME_SOURCE(0x2A13),
TIME_UPDATE_CONTROL_POINT(0x2A16),
TIME_UPDATE_STATE(0x2A17),
TIME_WITH_DST(0x2A11),
TIME_ZONE(0x2A0E),
TX_POWER_LEVEL(0x2A07),
UNREAD_ALERT_STATUS(0x2A45),
AGGREGATE_INPUT(0x2A5A),
ANALOG_INPUT(0x2A58),
ANALOG_OUTPUT(0x2A59),
DIGITAL_INPUT(0x2A56),
DIGITAL_OUTPUT(0x2A57),
EXACT_TIME_100(0x2A0B),
NETWORK_AVAILABILITY(0x2A3E),
SCIENTIFIC_TEMPERATURE_IN_CELSIUS(0x2A3C),
SECONDARY_TIME_ZONE(0x2A10),
STRING(0x2A3D),
TEMPERATURE_IN_CELSIUS(0x2A1F),
TEMPERATURE_IN_FAHRENHEIT(0x2A20),
TIME_BROADCAST(0x2A15),
BATTERY_LEVEL_STATE(0x2A1B),
BATTERY_POWER_STATE(0x2A1A),
PULSE_OXIMETRY_CONTINUOUS_MEASUREMENT(0x2A5F),
PULSE_OXIMETRY_CONTROL_POINT(0x2A62),
PULSE_OXIMETRY_FEATURES(0x2A61),
PULSE_OXIMETRY_PULSATILE_EVENT(0x2A60),
PULSE_OXIMETRY_SPOT_CHECK_MEASUREMENT(0x2A5E),
RECORD_ACCESS_CONTROL_POINT_TESTVERSION(0x2A52),
REMOVABLE(0x2A3A),
SERVICE_REQUIRED(0x2A3B);
private static Map<UUID, GattCharacteristic> uuidToServiceMapping;
private UUID uuid;
private GattCharacteristic(long key) {
this.uuid = new UUID((key << 32) | 0x1000, BluetoothBindingConstants.BLUETOOTH_BASE_UUID);
}
private static void initMapping() {
uuidToServiceMapping = new HashMap<>();
for (GattCharacteristic s : values()) {
uuidToServiceMapping.put(s.uuid, s);
}
}
public static GattCharacteristic getCharacteristic(UUID uuid) {
if (uuidToServiceMapping == null) {
initMapping();
}
return uuidToServiceMapping.get(uuid);
}
/**
* @return the key
*/
public UUID getUUID() {
return uuid;
}
}
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (c) 2010-2020 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;
/**
* Represents a Bluetooth class, which describes the general characteristics and capabilities of a device.
*
* @author Chris Jackson - Initial Contribution
*
*/
public class BluetoothClass {
private final int clazz;
public static final class Service {
private static final int BITMASK = 0xFFE000;
public static final int LIMITED_DISCOVERABILITY = 0x002000;
public static final int POSITIONING = 0x010000;
public static final int NETWORKING = 0x020000;
public static final int RENDER = 0x040000;
public static final int CAPTURE = 0x080000;
public static final int OBJECT_TRANSFER = 0x100000;
public static final int AUDIO = 0x200000;
public static final int TELEPHONY = 0x400000;
public static final int INFORMATION = 0x800000;
}
public static class Device {
private static final int BITMASK = 0x1FFC;
/**
* Defines the major device class constants.
*
*/
public static class Major {
private static final int BITMASK = 0x1F00;
public static final int MISC = 0x0000;
public static final int COMPUTER = 0x0100;
public static final int PHONE = 0x0200;
public static final int NETWORKING = 0x0300;
public static final int AUDIO_VIDEO = 0x0400;
public static final int PERIPHERAL = 0x0500;
public static final int IMAGING = 0x0600;
public static final int WEARABLE = 0x0700;
public static final int TOY = 0x0800;
public static final int HEALTH = 0x0900;
public static final int UNCATEGORIZED = 0x1F00;
}
// Devices in the COMPUTER major class
public static final int COMPUTER_UNCATEGORIZED = 0x0100;
public static final int COMPUTER_DESKTOP = 0x0104;
public static final int COMPUTER_SERVER = 0x0108;
public static final int COMPUTER_LAPTOP = 0x010C;
public static final int COMPUTER_HANDHELD_PC_PDA = 0x0110;
public static final int COMPUTER_PALM_SIZE_PC_PDA = 0x0114;
public static final int COMPUTER_WEARABLE = 0x0118;
// Devices in the PHONE major class
public static final int PHONE_UNCATEGORIZED = 0x0200;
public static final int PHONE_CELLULAR = 0x0204;
public static final int PHONE_CORDLESS = 0x0208;
public static final int PHONE_SMART = 0x020C;
public static final int PHONE_MODEM_OR_GATEWAY = 0x0210;
public static final int PHONE_ISDN = 0x0214;
// Minor classes for the AUDIO_VIDEO major class
public static final int AUDIO_VIDEO_UNCATEGORIZED = 0x0400;
public static final int AUDIO_VIDEO_WEARABLE_HEADSET = 0x0404;
public static final int AUDIO_VIDEO_HANDSFREE = 0x0408;
public static final int AUDIO_VIDEO_MICROPHONE = 0x0410;
public static final int AUDIO_VIDEO_LOUDSPEAKER = 0x0414;
public static final int AUDIO_VIDEO_HEADPHONES = 0x0418;
public static final int AUDIO_VIDEO_PORTABLE_AUDIO = 0x041C;
public static final int AUDIO_VIDEO_CAR_AUDIO = 0x0420;
public static final int AUDIO_VIDEO_SET_TOP_BOX = 0x0424;
public static final int AUDIO_VIDEO_HIFI_AUDIO = 0x0428;
public static final int AUDIO_VIDEO_VCR = 0x042C;
public static final int AUDIO_VIDEO_VIDEO_CAMERA = 0x0430;
public static final int AUDIO_VIDEO_CAMCORDER = 0x0434;
public static final int AUDIO_VIDEO_VIDEO_MONITOR = 0x0438;
public static final int AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x043C;
public static final int AUDIO_VIDEO_VIDEO_CONFERENCING = 0x0440;
public static final int AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x0448;
// Devices in the WEARABLE major class
public static final int WEARABLE_UNCATEGORIZED = 0x0700;
public static final int WEARABLE_WRIST_WATCH = 0x0704;
public static final int WEARABLE_PAGER = 0x0708;
public static final int WEARABLE_JACKET = 0x070C;
public static final int WEARABLE_HELMET = 0x0710;
public static final int WEARABLE_GLASSES = 0x0714;
// Devices in the TOY major class
public static final int TOY_UNCATEGORIZED = 0x0800;
public static final int TOY_ROBOT = 0x0804;
public static final int TOY_VEHICLE = 0x0808;
public static final int TOY_DOLL_ACTION_FIGURE = 0x080C;
public static final int TOY_CONTROLLER = 0x0810;
public static final int TOY_GAME = 0x0814;
// Devices in the HEALTH major class
public static final int HEALTH_UNCATEGORIZED = 0x0900;
public static final int HEALTH_BLOOD_PRESSURE = 0x0904;
public static final int HEALTH_THERMOMETER = 0x0908;
public static final int HEALTH_WEIGHING = 0x090C;
public static final int HEALTH_GLUCOSE = 0x0910;
public static final int HEALTH_PULSE_OXIMETER = 0x0914;
public static final int HEALTH_PULSE_RATE = 0x0918;
public static final int HEALTH_DATA_DISPLAY = 0x091C;
// Devices in PERIPHERAL major class
public static final int PERIPHERAL_NON_KEYBOARD_NON_POINTING = 0x0500;
public static final int PERIPHERAL_KEYBOARD = 0x0540;
public static final int PERIPHERAL_POINTING = 0x0580;
public static final int PERIPHERAL_KEYBOARD_POINTING = 0x05C0;
}
/**
* Public constructor
*
* @param clazz the device class provided in the bluetooth descriptor
*/
public BluetoothClass(int clazz) {
this.clazz = clazz;
}
/**
* Return the major and minor device class
*
* @return major and minor device class
*/
public int getDeviceClass() {
return (clazz & Device.BITMASK);
}
/**
* Return the major device class
*
* @return the major device class
*/
public int getMajorDeviceClass() {
return (clazz & Device.Major.BITMASK);
}
/**
* Return true if the specified service class is supported
*
* @param service the service id
* @return true, if the class supports the service
*/
public boolean hasService(int service) {
return ((clazz & Service.BITMASK & service) != 0);
}
}

View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) 2010-2020 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;
/**
* An enumeration of transaction completion status values
*
* @author Chris Jackson - Initial contribution
*
*/
public enum BluetoothCompletionStatus {
SUCCESS,
ERROR
}

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* The {@link BluetoothDescriptor} class defines the Bluetooth descriptor.
* <p>
* Descriptors are defined attributes that describe a characteristic value.
* <p>
* https://www.bluetooth.com/specifications/gatt/descriptors
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - added constructor and fixed setValue method
*/
public class BluetoothDescriptor {
protected final BluetoothCharacteristic characteristic;
protected final UUID uuid;
protected byte[] value;
/**
* The main constructor
*
* @param characteristic the characteristic that this class describes
* @param uuid the uuid of the descriptor
*/
public BluetoothDescriptor(BluetoothCharacteristic characteristic, UUID uuid) {
this.characteristic = characteristic;
this.uuid = uuid;
}
/**
* Returns the characteristic this descriptor belongs to.
*
* @return
*/
BluetoothCharacteristic getCharacteristic() {
return characteristic;
}
/**
* Returns the permissions for this descriptor.
*
* @return the permissions
*/
public int getPermissions() {
return 0;
}
/**
* Returns the UUID of this descriptor.
*
* @return the UUID
*/
public UUID getUuid() {
return uuid;
}
/**
* Returns the stored value for this descriptor. It doesn't read remote data.
*
* @return the value of the descriptor
*/
public byte[] getValue() {
return value;
}
/**
* Sets the stored value for this descriptor. It doesn't update remote data.
*
* @param value the value for this descriptor instance
*/
public void setValue(byte[] value) {
this.value = value;
}
public GattDescriptor getDescriptor() {
return GattDescriptor.getDescriptor(uuid);
}
public enum GattDescriptor {
// Descriptors
CHARACTERISTIC_EXTENDED_PROPERTIES(0x2900),
CHARACTERISTIC_USER_DESCRIPTION(0x2901),
CLIENT_CHARACTERISTIC_CONFIGURATION(0x2902),
SERVER_CHARACTERISTIC_CONFIGURATION(0x2903),
CHARACTERISTIC_PRESENTATION_FORMAT(0x2904),
CHARACTERISTIC_AGGREGATE_FORMAT(0x2905),
VALID_RANGE(0x2906),
EXTERNAL_REPORT_REFERENCE(0x2907),
REPORT_REFERENCE(0x2908),
NUMBER_OF_DIGITALS(0x2909),
TRIGGER_SETTING(0x290A);
private static Map<UUID, GattDescriptor> uuidToServiceMapping;
private final UUID uuid;
private GattDescriptor(long key) {
this.uuid = new UUID((key << 32) | 0x1000, BluetoothBindingConstants.BLUETOOTH_BASE_UUID);
}
private static void initMapping() {
uuidToServiceMapping = new HashMap<>();
for (GattDescriptor s : values()) {
uuidToServiceMapping.put(s.uuid, s);
}
}
public static GattDescriptor getDescriptor(UUID uuid) {
if (uuidToServiceMapping == null) {
initMapping();
}
return uuidToServiceMapping.get(uuid);
}
/**
* @return the key
*/
public UUID getUUID() {
return uuid;
}
}
}

View File

@@ -0,0 +1,411 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Collection;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BluetoothDevice} class provides a base implementation of a Bluetooth Low Energy device
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Refactored class to use Integer instead of int, fixed bugs, diverse improvements
* @author Connor Petty - Made most of the methods abstract
*/
@NonNullByDefault
public abstract class BluetoothDevice {
private final Logger logger = LoggerFactory.getLogger(BluetoothDevice.class);
/**
* Enumeration of Bluetooth connection states
*
*/
public enum ConnectionState {
/**
* Device is still being discovered and is not available for use.
*/
DISCOVERING,
/**
* Device has been discovered. This is used for the initial notification that the device is available.
*/
DISCOVERED,
/**
* Device is disconnected.
*/
DISCONNECTED,
/**
* A connection is in progress.
*/
CONNECTING,
/**
* The device is connected.
*/
CONNECTED,
/**
* A disconnection is in progress.
*/
DISCONNECTING
}
protected enum BluetoothEventType {
CONNECTION_STATE,
SCAN_RECORD,
CHARACTERISTIC_READ_COMPLETE,
CHARACTERISTIC_WRITE_COMPLETE,
CHARACTERISTIC_UPDATED,
DESCRIPTOR_UPDATED,
SERVICES_DISCOVERED,
ADAPTER_CHANGED
}
/**
* The adapter the device is accessed through
*/
protected final BluetoothAdapter adapter;
/**
* Devices Bluetooth address
*/
protected final BluetoothAddress address;
/**
* Construct a Bluetooth device taking the Bluetooth address
*
* @param adapter
* @param sender
*/
public BluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
this.address = address;
this.adapter = adapter;
}
/**
* Returns the name of the Bluetooth device.
*
* @return The devices name
*/
public abstract @Nullable String getName();
/**
* Returns the manufacturer ID of the device
*
* @return an integer with manufacturer ID of the device, or null if not known
*/
public abstract @Nullable Integer getManufacturerId();
/**
* Returns the last Transmit Power value or null if no transmit power has been received
*
* @return the last reported transmitter power value in dBm
*/
public abstract @Nullable Integer getTxPower();
/**
* Returns the last Receive Signal Strength Indicator (RSSI) value or null if no RSSI has been received
*
* @return the last RSSI value in dBm
*/
public abstract @Nullable Integer getRssi();
/**
* Returns the physical address of the device.
*
* @return The physical address of the device
*/
public BluetoothAddress getAddress() {
return address;
}
/**
* Returns the adapter through which the device is accessed
*
* @return The adapter through which the device is accessed
*/
public BluetoothAdapter getAdapter() {
return adapter;
}
/**
* Returns a {@link BluetoothService} if the requested service is supported
*
* @return the {@link BluetoothService} or null if the service is not supported.
*/
public abstract @Nullable BluetoothService getServices(UUID uuid);
/**
* Returns a list of supported service UUIDs
*
* @return list of supported {@link BluetoothService}s.
*/
public abstract Collection<BluetoothService> getServices();
/**
* Check if the device supports the specified service
*
* @param uuid the service {@link UUID}
* @return true if the service is supported
*/
public abstract boolean supportsService(UUID uuid);
/**
* Get the current connection state for this device
*
* @return the current {@link ConnectionState}
*/
public abstract ConnectionState getConnectionState();
/**
* Connects to a device. This is an asynchronous method. Once the connection state is updated, the
* {@link BluetoothDeviceListener.onConnectionState} method will be called with the connection state.
* <p>
* If the device is already connected, this will return false.
*
* @return true if the connection process is started successfully
*/
public abstract boolean connect();
/**
* Disconnects from a device. Once the connection state is updated, the
* {@link BluetoothDeviceListener.onConnectionState}
* method will be called with the connection state.
* <p>
* If the device is not currently connected, this will return false.
*
* @return true if the disconnection process is started successfully
*/
public abstract boolean disconnect();
/**
* Starts a discovery on a device. This will iterate through all services and characteristics to build up a view of
* the device.
* <p>
* This method should be called before attempting to read or write characteristics.
*
* @return true if the discovery process is started successfully
*/
public abstract boolean discoverServices();
/**
* Gets a Bluetooth characteristic if it is known.
* <p>
* Note that this method will not search for a characteristic in the remote device if it is not known.
* You must have previously connected to the device so that the device services and characteristics can
* be retrieved.
*
* @param uuid the {@link UUID} of the characteristic to return
* @return the {@link BluetoothCharacteristic} or null if the characteristic is not found in the device
*/
public @Nullable BluetoothCharacteristic getCharacteristic(UUID uuid) {
for (BluetoothService service : getServices()) {
if (service.providesCharacteristic(uuid)) {
return service.getCharacteristic(uuid);
}
}
return null;
}
/**
* Reads a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
* operation when one is already in progress will result in subsequent calls returning false.
* <p>
* This is an asynchronous method. Once the read is complete
* {@link BluetoothDeviceListener.onCharacteristicReadComplete}
* method will be called with the completion state.
* <p>
* Note that {@link BluetoothDeviceListener.onCharacteristicUpdate} will be called when the read value is received.
*
* @param characteristic the {@link BluetoothCharacteristic} to read.
* @return true if the characteristic read is started successfully
*/
public abstract boolean readCharacteristic(BluetoothCharacteristic characteristic);
/**
* Writes a characteristic. Only a single read or write operation can be requested at once. Attempting to perform an
* operation when one is already in progress will result in subsequent calls returning false.
* <p>
* This is an asynchronous method. Once the write is complete
* {@link BluetoothDeviceListener.onCharacteristicWriteComplete} method will be called with the completion state.
*
* @param characteristic the {@link BluetoothCharacteristic} to read.
* @return true if the characteristic write is started successfully
*/
public abstract boolean writeCharacteristic(BluetoothCharacteristic characteristic);
/**
* Enables notifications for a characteristic. Only a single read or write operation can be requested at once.
* Attempting to perform an operation when one is already in progress will result in subsequent calls returning
* false.
* <p>
* Notifications result in CHARACTERISTIC_UPDATED events to the listeners.
*
* @param characteristic the {@link BluetoothCharacteristic} to receive notifications for.
* @return true if the characteristic notification is started successfully
*/
public abstract boolean enableNotifications(BluetoothCharacteristic characteristic);
/**
* Disables notifications for a characteristic. Only a single read or write operation can be requested at once.
* Attempting to perform an operation when one is already in progress will result in subsequent calls returning
* false.
*
* @param characteristic the {@link BluetoothCharacteristic} to disable notifications for.
* @return true if the characteristic notification is stopped successfully
*/
public abstract boolean disableNotifications(BluetoothCharacteristic characteristic);
/**
* Enables notifications for a descriptor. Only a single read or write operation can be requested at once.
* Attempting to perform an operation when one is already in progress will result in subsequent calls returning
* false.
* <p>
* Notifications result in DESCRIPTOR_UPDATED events to the listeners.
*
* @param descriptor the {@link BluetoothDescriptor} to receive notifications for.
* @return true if the descriptor notification is started successfully
*/
public abstract boolean enableNotifications(BluetoothDescriptor descriptor);
/**
* Disables notifications for a descriptor. Only a single read or write operation can be requested at once.
* Attempting to perform an operation when one is already in progress will result in subsequent calls returning
* false.
*
* @param descriptor the {@link BluetoothDescriptor} to disable notifications for.
* @return true if the descriptor notification is stopped successfully
*/
public abstract boolean disableNotifications(BluetoothDescriptor descriptor);
/**
* Adds a service to the device.
*
* @param service the new {@link BluetoothService} to add
* @return true if the service was added or false if the service was already supported
*/
protected abstract boolean addService(BluetoothService service);
/**
* Gets a service based on the handle.
* This will return a service if the handle falls within the start and end handles for the service.
*
* @param handle the handle for the service
* @return the {@link BluetoothService} or null if the service was not found
*/
protected @Nullable BluetoothService getServiceByHandle(int handle) {
for (BluetoothService service : getServices()) {
if (service.getHandleStart() <= handle && service.getHandleEnd() >= handle) {
return service;
}
}
return null;
}
/**
* Gets a characteristic based on the handle.
*
* @param handle the handle for the characteristic
* @return the {@link BluetoothCharacteristic} or null if the characteristic was not found
*/
protected @Nullable BluetoothCharacteristic getCharacteristicByHandle(int handle) {
BluetoothService service = getServiceByHandle(handle);
if (service != null) {
return service.getCharacteristicByHandle(handle);
}
return null;
}
/**
* Adds a device listener
*
* @param listener the {@link BluetoothDeviceListener} to add
*/
public final void addListener(BluetoothDeviceListener listener) {
getListeners().add(listener);
}
/**
* Removes a device listener
*
* @param listener the {@link BluetoothDeviceListener} to remove
*/
public final void removeListener(BluetoothDeviceListener listener) {
getListeners().remove(listener);
}
/**
* Checks if this device has any listeners
*
* @return true if this device has listeners
*/
public final boolean hasListeners() {
return !getListeners().isEmpty();
}
/**
* Releases resources that this device is using.
*
*/
protected abstract void dispose();
protected abstract Collection<BluetoothDeviceListener> getListeners();
/**
* Notify the listeners of an event
*
* @param event the {@link BluetoothEventType} of this event
* @param args an array of arguments to pass to the callback
*/
protected void notifyListeners(BluetoothEventType event, Object... args) {
for (BluetoothDeviceListener listener : getListeners()) {
try {
switch (event) {
case SCAN_RECORD:
listener.onScanRecordReceived((BluetoothScanNotification) args[0]);
break;
case CONNECTION_STATE:
listener.onConnectionStateChange((BluetoothConnectionStatusNotification) args[0]);
break;
case SERVICES_DISCOVERED:
listener.onServicesDiscovered();
break;
case CHARACTERISTIC_READ_COMPLETE:
listener.onCharacteristicReadComplete((BluetoothCharacteristic) args[0],
(BluetoothCompletionStatus) args[1]);
break;
case CHARACTERISTIC_WRITE_COMPLETE:
listener.onCharacteristicWriteComplete((BluetoothCharacteristic) args[0],
(BluetoothCompletionStatus) args[1]);
break;
case CHARACTERISTIC_UPDATED:
listener.onCharacteristicUpdate((BluetoothCharacteristic) args[0]);
break;
case DESCRIPTOR_UPDATED:
listener.onDescriptorUpdate((BluetoothDescriptor) args[0]);
break;
case ADAPTER_CHANGED:
listener.onAdapterChanged((BluetoothAdapter) args[0]);
break;
}
} catch (Exception e) {
logger.error("Failed to inform listener '{}': {}", listener, e.getMessage(), e);
}
}
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
/**
* The {@link BluetoothDeviceListener} class defines the a callback interface where devices are notified of updates to a
* BLE device
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Added descriptor updates
*/
@NonNullByDefault
public interface BluetoothDeviceListener {
/**
* Called when a scan record is received for the device
*
* @param scanNotification the {@link BluetoothScanNotification} providing the scan packet information
*/
void onScanRecordReceived(BluetoothScanNotification scanNotification);
/**
* Called when the connection status changes
*
* @param connectionNotification the {@link BluetoothConnectionStatusNotification} providing the updated connection
* information
*/
void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification);
/**
* Called when a devices services and characteristics have been completely read
*/
void onServicesDiscovered();
/**
* Called when a read request completes
*
* @param characteristic the {@link BluetoothCharacteristic} that has completed the read request
* @param status the {@link BluetoothCompletionStatus} of the read request
*/
void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
/**
* Called when a write request completes
*
* @param characteristic the {@link BluetoothCharacteristic} that has completed the write request
* @param status the {@link BluetoothCompletionStatus} of the write request
*/
void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status);
/**
* Called when a characteristic value is received. Implementations should call this whenever a value
* is received from the BLE device even if there is no change to the value.
*
* @param characteristic the updated {@link BluetoothCharacteristic}
*/
void onCharacteristicUpdate(BluetoothCharacteristic characteristic);
/**
* Called when a descriptor value is received. Implementations should call this whenever a value
* is received from the BLE device even if there is no change to the value.
*
* @param characteristic the updated {@link BluetoothCharacteristic}
*/
void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor);
/**
* Called when the BluetoothAdapter for this BluetoothDevice changes.
* Implementations should call this whenever they change the adapter used by this device.
* Note: In general this is only called by a RoamingBluetoothDevice
*
* @param adapter the new {@link BluetoothAdapter} used by this device
*/
void onAdapterChanged(BluetoothAdapter adapter);
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This is a listener interface that is e.g. used by {@link BluetoothAdapter}s after discovering new devices.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Connor Petty - API improvements
*/
@NonNullByDefault
public interface BluetoothDiscoveryListener {
/**
* Reports the discovery of a new device.
*
* @param device the newly discovered {@link BluetoothDevice}
*/
void deviceDiscovered(BluetoothDevice device);
/**
* Reports the removal of a device
*
* @param device the removed {@link BluetoothDevice}
*/
void deviceRemoved(BluetoothDevice device);
}

View File

@@ -0,0 +1,286 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* The {@link BluetoothCharacteristic} class defines the BLE Service.
* <p>
* Services are collections of characteristics and relationships to other services that encapsulate the behavior of part
* of a device.
* <p>
* https://www.bluetooth.com/specifications/gatt/services
*
* @author Chris Jackson - Initial contribution
* @author Kai Kreuzer - Cleaned up code
*/
public class BluetoothService {
// The service UUID
private final UUID uuid;
/**
* The start handle for this service
*/
private final int handleStart;
/**
* The end handle for this service
*/
private final int handleEnd;
protected int instanceId;
/**
* Indicates if this is a primary service (true) or secondary service (false)
*/
protected boolean primaryService;
/**
* Map of {@link BluetoothCharacteristic}s supported in this service
*/
protected final Map<UUID, BluetoothCharacteristic> supportedCharacteristics = new ConcurrentHashMap<>();
/**
* Constructor
*
* @param uuid the uuid of the service
*/
public BluetoothService(UUID uuid) {
this(uuid, true, 0, 0);
}
/**
* Constructor
*
* @param uuid the uuid of the service
* @param primaryService true, if this service is a primary service
*/
public BluetoothService(UUID uuid, boolean primaryService) {
this(uuid, primaryService, 0, 0);
}
/**
* Constructor
*
* @param uuid the uuid of the service
* @param primaryService true, if this service is a primary service
* @param handleStart id of the lowest handle
* @param handleEnd id of the highest handle
*/
public BluetoothService(UUID uuid, boolean primaryService, int handleStart, int handleEnd) {
this.uuid = uuid;
this.primaryService = primaryService;
this.handleStart = handleStart;
this.handleEnd = handleEnd;
}
/**
* Get characteristic based on {@link UUID}
*
* @return the {@link BluetoothCharacteristic} with the requested {@link UUID}
*/
public BluetoothCharacteristic getCharacteristic(UUID uuid) {
return supportedCharacteristics.get(uuid);
}
/**
* Get list of characteristics of the service
*
* @return the list of {@link BluetoothCharacteristic}s
*/
public List<BluetoothCharacteristic> getCharacteristics() {
return new ArrayList<>(supportedCharacteristics.values());
}
/**
* Return the UUID of this service
*
* @return the {@link UUID} of the service
*/
public UUID getUuid() {
return uuid;
}
/**
* Gets the starting handle for this service
*
* @return the start handle
*/
public int getHandleStart() {
return handleStart;
}
/**
* Gets the end handle for this service
*
* @return the end handle
*/
public int getHandleEnd() {
return handleEnd;
}
/**
* Get the type of this service (primary/secondary)
*
* @return true if this is a primary service
*/
public boolean isPrimary() {
return primaryService;
}
/**
* Returns the instance ID for this service
*
* @return Instance ID of this service
*/
public int getInstanceId() {
return instanceId;
}
/**
* Checks if the service provides a specific characteristic
*
* @return true if the characteristic is provided in this service
*/
public boolean providesCharacteristic(UUID uuid) {
return supportedCharacteristics.containsKey(uuid);
}
/**
* Add a characteristic to this service
*
* @param characteristic The characteristics to be added
* @return true, if the characteristic was added to the service
*/
public boolean addCharacteristic(BluetoothCharacteristic characteristic) {
if (supportedCharacteristics.get(characteristic.getUuid()) != null) {
return false;
}
supportedCharacteristics.put(characteristic.getUuid(), characteristic);
characteristic.setService(this);
return true;
}
/**
* Gets a characteristic by the handle
*
* @param handle the handle of the characteristic to return
* @return return the {@link BluetoothCharacteristic} or null if not found
*/
public BluetoothCharacteristic getCharacteristicByHandle(int handle) {
synchronized (supportedCharacteristics) {
for (BluetoothCharacteristic characteristic : supportedCharacteristics.values()) {
if (characteristic.getHandle() == handle) {
return characteristic;
}
}
}
return null;
}
/**
* Gets the {@link GattService} for this service. This is an enum defining the available GATT services.
*
* @return the {@link GattService} relating to this service
*/
public GattService getService() {
return GattService.getService(uuid);
}
public enum GattService {
// List of GATT Services
ALERT_NOTIFICATION_SERVICE(0x1811),
AUTOMATION_IO(0x1815),
BATTERY_SERVICE(0x180F),
BLOOD_PRESSURE(0x1810),
BODY_COMPOSITION(0x181B),
BOND_MANAGEMENT(0x181E),
CONTINUOUS_GLUCOSE_MONITORING(0x181F),
CURRENT_TIME_SERVICE(0x1805),
CYCLING_POWER(0x1818),
CYCLING_SPEED_AND_CADENCE(0x1816),
DEVICE_INFORMATION(0x180A),
ENVIRONMENTAL_SENSING(0x181A),
GENERIC_ACCESS(0x1800),
GENERIC_ATTRIBUTE(0x1801),
GLUCOSE(0x1808),
HEALTH_THERMOMETER(0x1809),
HEART_RATE(0x180D),
HTTP_PROXY(0x1823),
HUMAN_INTERFACE_DEVICE(0x1812),
IMMEDIATE_ALERT(0x1802),
INDOOR_POSITIONING(0x1821),
INTERNET_PROTOCOL_SUPPORT(0x1820),
LINK_LOSS(0x1803L),
LOCATION_AND_NAVIGATION(0x1819),
NEXT_DST_CHANGE_SERVICE(0x1807),
PHONE_ALERT_STATUS_SERVICE(0x180E),
REFERENCE_TIME_UPDATE_SERVICE(0x1806),
RUNNING_SPEED_AND_CADENCE(0x1814),
SCAN_PARAMETERS(0x1813),
TX_POWER(0x1804),
USER_DATA(0x181C),
WEIGHT_SCALE(0x181D);
private static Map<UUID, GattService> uuidToServiceMapping;
private UUID uuid;
private GattService(long key) {
this.uuid = new UUID((key << 32) | 0x1000, BluetoothBindingConstants.BLUETOOTH_BASE_UUID);
}
private static void initMapping() {
uuidToServiceMapping = new HashMap<>();
for (GattService s : values()) {
uuidToServiceMapping.put(s.uuid, s);
}
}
public static GattService getService(UUID uuid) {
if (uuidToServiceMapping == null) {
initMapping();
}
return uuidToServiceMapping.get(uuid);
}
/**
* @return the key
*/
public UUID getUUID() {
return uuid;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("BluetoothService [uuid=");
builder.append(uuid);
builder.append(", handleStart=");
builder.append(handleStart);
builder.append(", handleEnd=");
builder.append(handleEnd);
builder.append(']');
return builder.toString();
}
}

View File

@@ -0,0 +1,263 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
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;
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.
*
* @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 })
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
private ScheduledFuture<?> connectionJob;
// internal flag for the service resolution status
protected volatile boolean resolved = false;
protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
public ConnectedBluetoothHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
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
}
updateRSSI();
}, 0, 30, TimeUnit.SECONDS);
}
@Override
public void dispose() {
if (connectionJob != null) {
connectionJob.cancel(true);
connectionJob = null;
}
scheduler.submit(() -> {
try {
deviceLock.lock();
if (device != null) {
device.removeListener(this);
device.disconnect();
device = null;
}
} finally {
deviceLock.unlock();
}
});
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
// Handle REFRESH
if (command == RefreshType.REFRESH) {
for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
if (characteristic.getGattCharacteristic() != null
&& channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
device.readCharacteristic(characteristic);
break;
}
}
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
}
@Override
protected void updateStatusBasedOnRssi(boolean receivedSignal) {
// if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
// we are connected.
if (receivedSignal) {
if (device.getConnectionState() == ConnectionState.CONNECTED) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
super.onConnectionStateChange(connectionNotification);
switch (connectionNotification.getConnectionState()) {
case DISCOVERED:
// The device is now known on the Bluetooth network, so we can do something...
scheduler.submit(() -> {
if (device.getConnectionState() != ConnectionState.CONNECTED) {
if (!device.connect()) {
logger.debug("Error connecting to device after discovery.");
}
}
});
break;
case CONNECTED:
updateStatus(ThingStatus.ONLINE);
scheduler.submit(() -> {
if (!resolved && !device.discoverServices()) {
logger.debug("Error while discovering services");
}
});
break;
case DISCONNECTED:
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
break;
default:
break;
}
}
@Override
public void onServicesDiscovered() {
super.onServicesDiscovered();
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());
}
}
}
@Override
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()));
}
}
} else {
logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
super.onCharacteristicWriteComplete(characteristic, status);
if (logger.isDebugEnabled()) {
logger.debug("Wrote {} to characteristic {} of device {}: {}",
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
if (logger.isDebugEnabled()) {
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
public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
super.onDescriptorUpdate(descriptor);
if (logger.isDebugEnabled()) {
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
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

@@ -0,0 +1,164 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link DelegateBluetoothDevice} is an abstract parent class for BluetoothDevice implementations
* that delegate their functions to other BluetoothDevice instances.
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public abstract class DelegateBluetoothDevice extends BluetoothDevice {
public DelegateBluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
super(adapter, address);
}
protected abstract @Nullable BluetoothDevice getDelegate();
@Override
public @Nullable String getName() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getName() : null;
}
@Override
public @Nullable Integer getManufacturerId() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getManufacturerId() : null;
}
@Override
public @Nullable Integer getRssi() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getRssi() : null;
}
@Override
public @Nullable Integer getTxPower() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getTxPower() : null;
}
@Override
public @Nullable BluetoothService getServices(UUID uuid) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getServices(uuid) : null;
}
@Override
public Collection<BluetoothService> getServices() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getServices() : Collections.emptySet();
}
@Override
public boolean supportsService(UUID uuid) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.supportsService(uuid) : false;
}
@Override
public ConnectionState getConnectionState() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getConnectionState() : ConnectionState.DISCOVERED;
}
@Override
public boolean connect() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.connect() : false;
}
@Override
public boolean disconnect() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.disconnect() : false;
}
@Override
public boolean discoverServices() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.discoverServices() : false;
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
BluetoothDevice delegate = getDelegate();
return delegate != null && delegate.readCharacteristic(characteristic);
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
BluetoothDevice delegate = getDelegate();
return delegate != null && delegate.writeCharacteristic(characteristic);
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.enableNotifications(characteristic) : false;
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.disableNotifications(characteristic) : false;
}
@Override
public boolean enableNotifications(BluetoothDescriptor descriptor) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.enableNotifications(descriptor) : false;
}
@Override
public boolean disableNotifications(BluetoothDescriptor descriptor) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.disableNotifications(descriptor) : false;
}
@Override
protected boolean addService(BluetoothService service) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.addService(service) : false;
}
@Override
protected Collection<BluetoothDeviceListener> getListeners() {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getListeners() : Collections.emptySet();
}
@Override
public @Nullable BluetoothCharacteristic getCharacteristic(UUID uuid) {
BluetoothDevice delegate = getDelegate();
return delegate != null ? delegate.getCharacteristic(uuid) : null;
}
@Override
protected void dispose() {
BluetoothDevice delegate = getDelegate();
if (delegate != null) {
delegate.dispose();
}
}
}

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCompanyIdentifiers;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.DelegateBluetoothDevice;
/**
* The {@link BluetoothDiscoveryDevice} is the BluetoothDevice subclass passed to
* BluetoothDiscoveryParticipants as part of discovery. It includes extra fields
* provided for the convenience of participant implementations.
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public class BluetoothDiscoveryDevice extends DelegateBluetoothDevice {
private BluetoothDevice delegate;
protected @Nullable String model;
protected @Nullable String serialNumber;
protected @Nullable String hardwareRevision;
protected @Nullable String firmwareRevision;
protected @Nullable String softwareRevision;
public BluetoothDiscoveryDevice(BluetoothDevice device) {
super(device.getAdapter(), device.getAddress());
this.delegate = device;
}
@Override
protected @NonNull BluetoothDevice getDelegate() {
return delegate;
}
/**
* Returns the model of the Bluetooth device.
*
* @return The devices model, null if not known
*/
public @Nullable String getModel() {
return model;
}
/**
* Returns the serial number of the Bluetooth device.
*
* @return The serial model, null if not known
*/
public @Nullable String getSerialNumber() {
return serialNumber;
}
/**
* Returns the hardware revision of the Bluetooth device.
*
* @return The hardware revision, null if not known
*/
public @Nullable String getHardwareRevision() {
return hardwareRevision;
}
/**
* Returns the firmware revision of the Bluetooth device.
*
* @return The firmware revision, null if not known
*/
public @Nullable String getFirmwareRevision() {
return firmwareRevision;
}
/**
* Returns the software revision of the Bluetooth device.
*
* @return The software revision, null if not known
*/
public @Nullable String getSoftwareRevision() {
return softwareRevision;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("BluetoothDevice [address=");
builder.append(address);
builder.append(", manufacturer=");
Integer manufacturer = getManufacturerId();
builder.append(manufacturer);
if (BluetoothCompanyIdentifiers.get(manufacturer) != null) {
builder.append(" (");
builder.append(BluetoothCompanyIdentifiers.get(manufacturer));
builder.append(')');
}
builder.append(", name=");
builder.append(getName());
builder.append(", model=");
builder.append(model);
builder.append(", serialNumber=");
builder.append(serialNumber);
builder.append(", hardwareRevision=");
builder.append(hardwareRevision);
builder.append(", firmwareRevision=");
builder.append(firmwareRevision);
builder.append(", softwareRevision=");
builder.append(softwareRevision);
builder.append(", rssi=");
builder.append(getRssi());
builder.append(']');
return builder.toString();
}
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 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.discovery;
import java.util.Set;
import java.util.function.BiConsumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
/**
* A {@link BluetoothDiscoveryParticipant} that is registered as a service is picked up by the BluetoothDiscoveryService
* and can thus contribute {@link DiscoveryResult}s from Bluetooth scans.
*
* @author Kai Kreuzer - Initial contribution
* @author Connor Petty - added 'requiresConnection' and 'publishAdditionalResults' methods
*/
@NonNullByDefault
public interface BluetoothDiscoveryParticipant {
/**
* Defines the list of thing types that this participant can identify
*
* @return a set of thing type UIDs for which results can be created
*/
public Set<ThingTypeUID> getSupportedThingTypeUIDs();
/**
* Creates a discovery result for a Bluetooth device
*
* @param device the Bluetooth device found on the network
* @return the according discovery result or <code>null</code>, if device is not
* supported by this participant
*/
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device);
/**
* Returns the thing UID for a Bluetooth device
*
* @param device the Bluetooth device
* @return a thing UID or <code>null</code>, if the device is not supported by this participant
*/
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device);
/**
* Returns true if this participant requires the device to be connected before it can produce a
* DiscoveryResult (or null) from {@link createResult(BluetoothDevice)}.
* <p>
* Implementors should only return 'true' conservatively, and make sure to return 'false' in circumstances where a
* 'null' result would be guaranteed from {@link createResult(BluetoothDevice)} even if a connection was available
* (e.g. the advertised manufacturerId already mismatches).
* <p>
* In general, returning 'true' is equivalent to saying <i>"the device might match, but I need a connection to
* make sure"</i>.
*
* @param device the Bluetooth device
* @return true if a connection is required before calling {@link createResult(BluetoothDevice)}
*/
public default boolean requiresConnection(BluetoothDiscoveryDevice device) {
return false;
}
/**
* Allows participants to perform any post-processing on each DiscoveryResult as well
* as produce additional DiscoveryResults as they see fit.
* Additional results can be published using the provided {@code publisher}.
* Results published in this way will create a new DiscoveryResult and ThingUID
* using the provided {@link BluetoothAdapter} as the bridge instead.
* A BluetoothAdapter instance must be provided for any additional results sent to the publisher.
* <p>
* Note: Any additional results will not be subject to post-processing.
*
* @param result the DiscoveryResult to post-process
* @param publisher the consumer to publish additional results to.
*/
public default void publishAdditionalResults(DiscoveryResult result,
BiConsumer<BluetoothAdapter, DiscoveryResult> publisher) {
// do nothing by default
}
}

View File

@@ -0,0 +1,251 @@
/**
* Copyright (c) 2010-2020 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.discovery.internal;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
/**
* The {@link BluetoothDeviceSnapshot} acts as a dummy {@link BluetoothDevice} implementation that simply acts as a
* snapshot for device data at the time of creation.
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public class BluetoothDeviceSnapshot extends BluetoothDiscoveryDevice {
private @Nullable String name;
private @Nullable Integer manufacturer;
private @Nullable Integer txPower;
public BluetoothDeviceSnapshot(BluetoothDevice device) {
super(device);
this.txPower = device.getTxPower();
this.manufacturer = device.getManufacturerId();
this.name = device.getName();
}
@Override
public @Nullable String getName() {
return name;
}
/**
* Set the name of the device
*
* @param name a {@link String} defining the device name
*/
public void setName(String name) {
this.name = name;
}
/**
* Sets the manufacturer id for the device
*
* @param manufacturer the manufacturer id
*/
public void setManufacturerId(int manufacturer) {
this.manufacturer = manufacturer;
}
/**
* Returns the manufacturer ID of the device
*
* @return an integer with manufacturer ID of the device, or null if not known
*/
@Override
public @Nullable Integer getManufacturerId() {
return manufacturer;
}
/**
* Sets the device transmit power
*
* @param power the current transmitter power in dBm
*/
public void setTxPower(int txPower) {
this.txPower = txPower;
}
/**
* Returns the last Transmit Power value or null if no transmit power has been received
*
* @return the last reported transmitter power value in dBm
*/
@Override
public @Nullable Integer getTxPower() {
return txPower;
}
/**
* Set the model of the device
*
* @param model a {@link String} defining the device model
*/
public void setModel(String model) {
this.model = model;
}
/**
* Set the serial number of the device
*
* @param model a {@link String} defining the serial number
*/
public void setSerialNumberl(String serialNumber) {
this.serialNumber = serialNumber;
}
/**
* Set the hardware revision of the device
*
* @param model a {@link String} defining the hardware revision
*/
public void setHardwareRevision(String hardwareRevision) {
this.hardwareRevision = hardwareRevision;
}
/**
* Set the firmware revision of the device
*
* @param model a {@link String} defining the firmware revision
*/
public void setFirmwareRevision(String firmwareRevision) {
this.firmwareRevision = firmwareRevision;
}
/**
* Set the software revision of the device
*
* @param model a {@link String} defining the software revision
*/
public void setSoftwareRevision(String softwareRevision) {
this.softwareRevision = softwareRevision;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
BluetoothAddress address = this.address;
Integer manufacturer = this.manufacturer;
Integer txPower = this.txPower;
String name = this.name;
String model = this.model;
String serialNumber = this.serialNumber;
String hardwareRevision = this.hardwareRevision;
String firmwareRevision = this.firmwareRevision;
String softwareRevision = this.softwareRevision;
result = prime * result + address.hashCode();
result = prime * result + ((firmwareRevision == null) ? 0 : firmwareRevision.hashCode());
result = prime * result + ((hardwareRevision == null) ? 0 : hardwareRevision.hashCode());
result = prime * result + ((manufacturer == null) ? 0 : manufacturer.hashCode());
result = prime * result + ((model == null) ? 0 : model.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((serialNumber == null) ? 0 : serialNumber.hashCode());
result = prime * result + ((softwareRevision == null) ? 0 : softwareRevision.hashCode());
result = prime * result + ((txPower == null) ? 0 : txPower.hashCode());
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
BluetoothDeviceSnapshot other = (BluetoothDeviceSnapshot) obj;
if (!Objects.equals(address, other.address)) {
return false;
}
if (!Objects.equals(manufacturer, other.manufacturer)) {
return false;
}
if (!Objects.equals(txPower, other.txPower)) {
return false;
}
if (!Objects.equals(name, other.name)) {
return false;
}
if (!Objects.equals(model, other.model)) {
return false;
}
if (!Objects.equals(serialNumber, other.serialNumber)) {
return false;
}
if (!Objects.equals(hardwareRevision, other.hardwareRevision)) {
return false;
}
if (!Objects.equals(firmwareRevision, other.firmwareRevision)) {
return false;
}
if (!Objects.equals(softwareRevision, other.softwareRevision)) {
return false;
}
return true;
}
/**
* This merges non-null identity fields from the given device into this snapshot.
*
* @return true if this snapshot changed as a result of this operation
*/
public void merge(BluetoothDeviceSnapshot device) {
Integer txPower = device.getTxPower();
Integer manufacturer = device.getManufacturerId();
String name = device.getName();
String model = device.getModel();
String serialNumber = device.getSerialNumber();
String hardwareRevision = device.getHardwareRevision();
String firmwareRevision = device.getFirmwareRevision();
String softwareRevision = device.getSoftwareRevision();
if (this.txPower == null && txPower != null) {
this.txPower = txPower;
}
if (this.manufacturer == null && manufacturer != null) {
this.manufacturer = manufacturer;
}
if (this.name == null && name != null) {
this.name = name;
}
if (this.model == null && model != null) {
this.model = model;
}
if (this.serialNumber == null && serialNumber != null) {
this.serialNumber = serialNumber;
}
if (this.hardwareRevision == null && hardwareRevision != null) {
this.hardwareRevision = hardwareRevision;
}
if (this.firmwareRevision == null && firmwareRevision != null) {
this.firmwareRevision = firmwareRevision;
}
if (this.softwareRevision == null && softwareRevision != null) {
this.softwareRevision = softwareRevision;
}
}
}

View File

@@ -0,0 +1,378 @@
/**
* Copyright (c) 2010-2020 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.discovery.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
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;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
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.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BluetoothDiscoveryProcess} does the work of creating a DiscoveryResult from a set of
* {@link BluetoothDisocveryParticipant}s
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public class BluetoothDiscoveryProcess implements Supplier<DiscoveryResult>, BluetoothDeviceListener {
private static final int DISCOVERY_TTL = 300;
private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryProcess.class);
private final Lock serviceDiscoveryLock = new ReentrantLock();
private final Condition connectionCondition = serviceDiscoveryLock.newCondition();
private final Condition serviceDiscoveryCondition = serviceDiscoveryLock.newCondition();
private final Condition infoDiscoveryCondition = serviceDiscoveryLock.newCondition();
private final BluetoothDeviceSnapshot device;
private final Collection<BluetoothDiscoveryParticipant> participants;
private final Set<BluetoothAdapter> adapters;
private volatile boolean servicesDiscovered = false;
/**
* Contains characteristic which reading is ongoing or null if no ongoing readings.
*/
private volatile @Nullable GattCharacteristic ongoingGattCharacteristic;
public BluetoothDiscoveryProcess(BluetoothDeviceSnapshot device,
Collection<BluetoothDiscoveryParticipant> participants, Set<BluetoothAdapter> adapters) {
this.participants = participants;
this.device = device;
this.adapters = adapters;
}
@Override
public DiscoveryResult get() {
// 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) {
if (participant.requiresConnection(device)) {
connectionParticipants.add(participant);
continue;
}
try {
DiscoveryResult result = participant.createResult(device);
if (result != null) {
return result;
}
} catch (RuntimeException e) {
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
}
}
// 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);
}
}
}
}
if (result == null) {
result = createDefaultResult(device);
}
return result;
}
private boolean isAddressAvailable(BluetoothAddress address) {
// if a device with this address has a handler on any of the adapters, we abandon discovery
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
String label = device.getName();
if (label == null || label.length() == 0 || label.equals(device.getAddress().toString().replace(':', '-'))) {
label = "Bluetooth Device";
}
Map<String, Object> properties = new HashMap<>();
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
Integer txPower = device.getTxPower();
if (txPower != null && txPower > 0) {
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
}
String manufacturer = BluetoothCompanyIdentifiers.get(device.getManufacturerId());
if (manufacturer == null) {
logger.debug("Unknown manufacturer Id ({}) found on bluetooth device.", device.getManufacturerId());
} else {
properties.put(Thing.PROPERTY_VENDOR, manufacturer);
label += " (" + manufacturer + ")";
}
ThingUID thingUID = new ThingUID(BluetoothBindingConstants.THING_TYPE_BEACON, 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();
}
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);
}
try {
DiscoveryResult result = participant.createResult(device);
if (result != null) {
return result;
}
} catch (RuntimeException e) {
logger.warn("Participant '{}' threw an exception", participant.getClass().getName(), e);
}
}
} catch (InterruptedException e) {
// do nothing
} finally {
device.removeListener(this);
}
return null;
}
@Override
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
}
@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
serviceDiscoveryLock.lock();
try {
connectionCondition.signal();
} finally {
serviceDiscoveryLock.unlock();
}
}
}
private void readDeviceInformationIfMissing() throws InterruptedException {
if (device.getName() == null) {
fecthGattCharacteristic(GattCharacteristic.DEVICE_NAME);
}
if (device.getModel() == null) {
fecthGattCharacteristic(GattCharacteristic.MODEL_NUMBER_STRING);
}
if (device.getSerialNumber() == null) {
fecthGattCharacteristic(GattCharacteristic.SERIAL_NUMBER_STRING);
}
if (device.getHardwareRevision() == null) {
fecthGattCharacteristic(GattCharacteristic.HARDWARE_REVISION_STRING);
}
if (device.getFirmwareRevision() == null) {
fecthGattCharacteristic(GattCharacteristic.FIRMWARE_REVISION_STRING);
}
if (device.getSoftwareRevision() == null) {
fecthGattCharacteristic(GattCharacteristic.SOFTWARE_REVISION_STRING);
}
}
private void fecthGattCharacteristic(GattCharacteristic gattCharacteristic) throws InterruptedException {
UUID uuid = gattCharacteristic.getUUID();
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
if (characteristic == null) {
logger.debug("Device '{}' doesn't support uuid '{}'", device.getAddress(), uuid);
return;
}
if (!device.readCharacteristic(characteristic)) {
logger.debug("Failed to aquire uuid {} from device {}", uuid, device.getAddress());
return;
}
ongoingGattCharacteristic = gattCharacteristic;
if (!awaitInfoResponse(1, TimeUnit.SECONDS)) {
logger.debug("Device info (uuid {}) for device {} timed out", uuid, device.getAddress());
ongoingGattCharacteristic = null;
}
}
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
serviceDiscoveryLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (device.getConnectionState() != ConnectionState.CONNECTED) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
}
} finally {
serviceDiscoveryLock.unlock();
}
return true;
}
private boolean awaitInfoResponse(long timeout, TimeUnit unit) throws InterruptedException {
serviceDiscoveryLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (ongoingGattCharacteristic != null) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = infoDiscoveryCondition.awaitNanos(nanosTimeout);
}
} finally {
serviceDiscoveryLock.unlock();
}
return true;
}
private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
serviceDiscoveryLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (!servicesDiscovered) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
}
} finally {
serviceDiscoveryLock.unlock();
}
return true;
}
@Override
public void onServicesDiscovered() {
serviceDiscoveryLock.lock();
try {
servicesDiscovered = true;
serviceDiscoveryCondition.signal();
} finally {
serviceDiscoveryLock.unlock();
}
}
@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
serviceDiscoveryLock.lock();
try {
if (status == BluetoothCompletionStatus.SUCCESS) {
switch (characteristic.getGattCharacteristic()) {
case DEVICE_NAME:
device.setName(characteristic.getStringValue(0));
break;
case MODEL_NUMBER_STRING:
device.setModel(characteristic.getStringValue(0));
break;
case SERIAL_NUMBER_STRING:
device.setSerialNumberl(characteristic.getStringValue(0));
break;
case HARDWARE_REVISION_STRING:
device.setHardwareRevision(characteristic.getStringValue(0));
break;
case FIRMWARE_REVISION_STRING:
device.setFirmwareRevision(characteristic.getStringValue(0));
break;
case SOFTWARE_REVISION_STRING:
device.setSoftwareRevision(characteristic.getStringValue(0));
break;
default:
break;
}
}
if (ongoingGattCharacteristic == characteristic.getGattCharacteristic()) {
ongoingGattCharacteristic = null;
infoDiscoveryCondition.signal();
}
} finally {
serviceDiscoveryLock.unlock();
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
}
@Override
public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
}
@Override
public void onAdapterChanged(BluetoothAdapter adapter) {
}
}

View File

@@ -0,0 +1,314 @@
/**
* Copyright (c) 2010-2020 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.discovery.internal;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiConsumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothDiscoveryListener;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BluetoothDiscoveryService} handles searching for BLE devices.
*
* @author Chris Jackson - Initial Contribution
* @author Kai Kreuzer - Introduced BluetoothAdapters and BluetoothDiscoveryParticipants
* @author Connor Petty - Introduced connection based discovery and added roaming support
*/
@NonNullByDefault
@Component(immediate = true, service = DiscoveryService.class, configurationPid = "discovery.bluetooth")
public class BluetoothDiscoveryService extends AbstractDiscoveryService implements BluetoothDiscoveryListener {
private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryService.class);
private static final int SEARCH_TIME = 15;
private final Set<BluetoothAdapter> adapters = new CopyOnWriteArraySet<>();
private final Set<BluetoothDiscoveryParticipant> participants = new CopyOnWriteArraySet<>();
@NonNullByDefault({})
private final Map<BluetoothAddress, DiscoveryCache> discoveryCaches = new ConcurrentHashMap<>();
private final Set<ThingTypeUID> supportedThingTypes = new CopyOnWriteArraySet<>();
public BluetoothDiscoveryService() {
super(SEARCH_TIME);
supportedThingTypes.add(BluetoothBindingConstants.THING_TYPE_BEACON);
}
@Override
@Activate
protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
logger.debug("Activating Bluetooth discovery service");
super.activate(configProperties);
}
@Override
@Modified
protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
super.modified(configProperties);
}
@Override
@Deactivate
public void deactivate() {
logger.debug("Deactivating Bluetooth discovery service");
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addBluetoothAdapter(BluetoothAdapter adapter) {
this.adapters.add(adapter);
adapter.addDiscoveryListener(this);
}
protected void removeBluetoothAdapter(BluetoothAdapter adapter) {
this.adapters.remove(adapter);
adapter.removeDiscoveryListener(this);
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addBluetoothDiscoveryParticipant(BluetoothDiscoveryParticipant participant) {
this.participants.add(participant);
supportedThingTypes.addAll(participant.getSupportedThingTypeUIDs());
}
protected void removeBluetoothDiscoveryParticipant(BluetoothDiscoveryParticipant participant) {
supportedThingTypes.removeAll(participant.getSupportedThingTypeUIDs());
this.participants.remove(participant);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return supportedThingTypes;
}
@Override
public void startScan() {
for (BluetoothAdapter adapter : adapters) {
adapter.scanStart();
}
}
@Override
public void stopScan() {
for (BluetoothAdapter adapter : adapters) {
adapter.scanStop();
}
removeOlderResults(getTimestampOfLastScan());
}
@Override
public void deviceRemoved(BluetoothDevice device) {
discoveryCaches.computeIfPresent(device.getAddress(), (addr, cache) -> cache.removeDiscoveries(device));
}
@Override
public void deviceDiscovered(BluetoothDevice device) {
logger.debug("Discovered bluetooth device '{}': {}", device.getName(), device);
DiscoveryCache cache = discoveryCaches.computeIfAbsent(device.getAddress(), addr -> new DiscoveryCache());
cache.handleDiscovery(device);
}
private static ThingUID createThingUIDWithBridge(DiscoveryResult result, BluetoothAdapter adapter) {
return new ThingUID(result.getThingTypeUID(), adapter.getUID(), result.getThingUID().getId());
}
private static DiscoveryResult copyWithNewBridge(DiscoveryResult result, BluetoothAdapter adapter) {
String label = result.getLabel();
String adapterLabel = adapter.getLabel();
if (adapterLabel != null) {
label = adapterLabel + " - " + label;
}
return DiscoveryResultBuilder.create(createThingUIDWithBridge(result, adapter))//
.withBridge(adapter.getUID())//
.withProperties(result.getProperties())//
.withRepresentationProperty(result.getRepresentationProperty())//
.withTTL(result.getTimeToLive())//
.withLabel(label)//
.build();
}
private class DiscoveryCache {
private final Map<BluetoothAdapter, SnapshotFuture> discoveryFutures = new HashMap<>();
private final Map<BluetoothAdapter, @Nullable Set<DiscoveryResult>> discoveryResults = new ConcurrentHashMap<>();
private @Nullable BluetoothDeviceSnapshot latestSnapshot;
/**
* This is meant to be used as part of a Map.compute function
*
* @param device the device to remove from this cache
* @return this DiscoveryCache if there are still snapshots, null otherwise
*/
public synchronized @Nullable DiscoveryCache removeDiscoveries(final BluetoothDevice device) {
// we remove any discoveries that have been published for this device
BluetoothAdapter adapter = device.getAdapter();
if (discoveryFutures.containsKey(adapter)) {
discoveryFutures.remove(adapter).future.thenAccept(result -> retractDiscoveryResult(adapter, result));
}
if (discoveryFutures.isEmpty()) {
return null;
}
return this;
}
public synchronized void handleDiscovery(BluetoothDevice device) {
if (!discoveryFutures.isEmpty()) {
CompletableFuture
// we have an ongoing futures so lets create our discovery after they all finish
.allOf(discoveryFutures.values().stream().map(sf -> sf.future)
.toArray(CompletableFuture[]::new))
.thenRun(() -> createDiscoveryFuture(device));
} else {
createDiscoveryFuture(device);
}
}
private synchronized void createDiscoveryFuture(BluetoothDevice device) {
BluetoothAdapter adapter = device.getAdapter();
CompletableFuture<DiscoveryResult> future = null;
BluetoothDeviceSnapshot snapshot = new BluetoothDeviceSnapshot(device);
BluetoothDeviceSnapshot latestSnapshot = this.latestSnapshot;
if (latestSnapshot != null) {
snapshot.merge(latestSnapshot);
if (snapshot.equals(latestSnapshot)) {
// this means that snapshot has no newer fields than the latest snapshot
if (discoveryFutures.containsKey(adapter)
&& discoveryFutures.get(adapter).snapshot.equals(latestSnapshot)) {
// This adapter has already produced the most up-to-date result, so no further processing is
// necessary
return;
}
/*
* This isn't a new snapshot, but an up-to-date result from this adapter has not been produced yet.
* Since a result must have been produced for this snapshot, we search the results of the other
* adapters to find the future for the latest snapshot, then we modify it to make it look like it
* came from this adapter. This way we don't need to recompute the DiscoveryResult.
*/
Optional<CompletableFuture<DiscoveryResult>> otherFuture = discoveryFutures.values().stream()
// make sure that we only get futures for the current snapshot
.filter(sf -> sf.snapshot.equals(latestSnapshot)).findAny().map(sf -> sf.future);
if (otherFuture.isPresent()) {
future = otherFuture.get();
}
}
}
this.latestSnapshot = snapshot;
if (future == null) {
// we pass in the snapshot since it acts as a delegate for the device. It will also retain any new
// fields added to the device as part of the discovery process.
future = startDiscoveryProcess(snapshot);
}
if (discoveryFutures.containsKey(adapter)) {
// now we need to make sure that we remove the old discovered result if it is different from the new
// one.
SnapshotFuture oldSF = discoveryFutures.get(adapter);
future = oldSF.future.thenCombine(future, (oldResult, newResult) -> {
logger.trace("\n old: {}\n new: {}", oldResult, newResult);
if (!oldResult.getThingUID().equals(newResult.getThingUID())) {
retractDiscoveryResult(adapter, oldResult);
}
return newResult;
});
}
/*
* this appends a post-process to any ongoing or completed discoveries with this device's address.
* If this discoveryFuture is ongoing then this post-process will run asynchronously upon the future's
* completion.
* If this discoveryFuture is already completed then this post-process will run in the current thread.
* We need to make sure that this is part of the future chain so that the call to 'thingRemoved'
* in the 'removeDiscoveries' method above can be sure that it is running after the 'thingDiscovered'
*/
future = future.thenApply(result -> {
publishDiscoveryResult(adapter, result);
return result;
});
// now save this snapshot for later
discoveryFutures.put(adapter, new SnapshotFuture(snapshot, future));
}
private void publishDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult result) {
Set<DiscoveryResult> results = new HashSet<>();
BiConsumer<BluetoothAdapter, DiscoveryResult> publisher = (a, r) -> {
results.add(copyWithNewBridge(r, a));
};
publisher.accept(adapter, result);
for (BluetoothDiscoveryParticipant participant : participants) {
participant.publishAdditionalResults(result, publisher);
}
results.forEach(BluetoothDiscoveryService.this::thingDiscovered);
discoveryResults.put(adapter, results);
}
private void retractDiscoveryResult(BluetoothAdapter adapter, DiscoveryResult result) {
Set<DiscoveryResult> results = discoveryResults.remove(adapter);
if (results != null) {
for (DiscoveryResult r : results) {
thingRemoved(r.getThingUID());
}
}
}
private CompletableFuture<DiscoveryResult> startDiscoveryProcess(BluetoothDeviceSnapshot device) {
return CompletableFuture.supplyAsync(new BluetoothDiscoveryProcess(device, participants, adapters),
scheduler);
}
}
private static class SnapshotFuture {
public final BluetoothDeviceSnapshot snapshot;
public final CompletableFuture<DiscoveryResult> future;
public SnapshotFuture(BluetoothDeviceSnapshot snapshot, CompletableFuture<DiscoveryResult> future) {
this.snapshot = snapshot;
this.future = future;
}
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.internal;
import java.util.HashSet;
import java.util.Set;
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;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link BluetoothHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Kai Kreuzer - Initial contribution and API
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bluetooth")
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
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
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;
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 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.notification;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
/**
* The {@link BluetoothConnectionStatusNotification} provides a notification of a change in the device connection state.
*
* @author Chris Jackson - Initial contribution
*/
public class BluetoothConnectionStatusNotification extends BluetoothNotification {
private ConnectionState connectionState;
public BluetoothConnectionStatusNotification(ConnectionState connectionState) {
this.connectionState = connectionState;
}
/**
* Returns the connection state for this notification
*
* @return the {@link ConnectionState}
*/
public ConnectionState getConnectionState() {
return connectionState;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 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.notification;
import org.openhab.binding.bluetooth.BluetoothAddress;
/**
* The {@link BluetoothNotification} is the base class for Bluetooth device notifications
*
* @author Chris Jackson - Initial contribution
*/
public abstract class BluetoothNotification {
protected BluetoothAddress address;
/**
* Returns the bluetooth address for this frame
*/
public BluetoothAddress getAddress() {
return address;
}
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright (c) 2010-2020 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.notification;
/**
* The {@link BluetoothScanNotification} provides a notification of a received scan packet
*
* @author Chris Jackson - Initial contribution
*/
public class BluetoothScanNotification extends BluetoothNotification {
/**
* The receive signal strength for this beacon packet
*/
private int rssi = Integer.MIN_VALUE;
/**
* The raw data
*/
private byte[] data = null;
/**
* The manufacturer specific data
*/
private byte[] manufacturerData = null;
/**
* The beacon type
*/
private BluetoothBeaconType beaconType = BluetoothBeaconType.BEACON_UNKNOWN;
/**
* The device name
*/
private String name = new String();
/**
* An enumeration of basic beacon types
*/
public enum BluetoothBeaconType {
BEACON_UNKNOWN,
BEACON_ADVERTISEMENT,
BEACON_SCANRESPONSE
}
/**
* Sets the receive signal strength RSSI value for the scan
*
* param rssi the RSSI value for the scan packet in dBm
*/
public void setRssi(int rssi) {
this.rssi = rssi;
}
/**
* Gets the receive signal strength RSSI value for the scan
*
* @return the RSSI value for the scan packet in dBm or Integer.MIN_VALUE if no RSSI is available.
*/
public int getRssi() {
return rssi;
}
/**
* Sets the scan packet data
*
* @param data a byte array containing the raw packet data;
*/
public void setData(byte[] data) {
this.data = data;
}
/**
* Gets the scan packet data
*
* @return a byte array containing the data or null if none is set
*/
public byte[] getData() {
return data;
}
/**
* Sets the scan packet manufacturer specific data
*
* @param manufacturerData a byte array containing the manufacturer specific data
*/
public void setManufacturerData(byte[] manufacturerData) {
this.manufacturerData = manufacturerData;
}
/**
* Gets the scan packet manufacturer specific data
*
* @return a byte array containing the manufacturer specific data or null if none is set
*/
public byte[] getManufacturerData() {
return manufacturerData;
}
/**
* Sets the beacon type for this packet
*
* @beaconType the {@link BluetoothBeaconType} for this packet
*/
public void setBeaconType(BluetoothBeaconType beaconType) {
this.beaconType = beaconType;
}
/**
* Gets the beacon type for this packet
*
* @return the {@link BluetoothBeaconType} for this packet
*/
public BluetoothBeaconType getBeaconType() {
return beaconType;
}
/**
* Sets the device name
*
* @param name {@link String} containing the device name
*/
public void setDeviceName(String name) {
this.name = name;
}
/**
* Gets the device name
*
* @return {@link String} containing the device name
*/
public String getDeviceName() {
return name;
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="bluetooth" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Bluetooth Binding</name>
<description>This binding supports the Bluetooth protocol.</description>
</binding:binding>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bluetooth"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="rssi">
<item-type>Number</item-type>
<label>RSSI</label>
<description>Received signal strength indicator</description>
<category>QualityOfService</category>
<state readOnly="true" pattern="%d dBm"/>
</channel-type>
<channel-type id="adapter-uid">
<item-type>String</item-type>
<label>Adapter UID</label>
<description>ThingUID of nearest adapter</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="adapter-location">
<item-type>String</item-type>
<label>Adapter Location</label>
<description>Location of the nearest adapter to this device</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bluetooth"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="beacon">
<supported-bridge-type-refs>
<bridge-type-ref id="roaming"/>
<bridge-type-ref id="bluegiga"/>
<bridge-type-ref id="bluez"/>
</supported-bridge-type-refs>
<label>Bluetooth Device</label>
<description>A generic Bluetooth device in beacon-mode</description>
<channels>
<channel id="rssi" typeId="rssi"/>
</channels>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The unique Bluetooth address of the device</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="connected">
<supported-bridge-type-refs>
<bridge-type-ref id="roaming"/>
<bridge-type-ref id="bluegiga"/>
<bridge-type-ref id="bluez"/>
</supported-bridge-type-refs>
<label>Connected Bluetooth Device</label>
<description>A generic Bluetooth device in connected-mode</description>
<config-description>
<parameter name="address" type="text">
<label>Address</label>
<description>The unique Bluetooth address of the device</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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;
import org.junit.Test;
/**
* Tests {@link BluetoothAddress}.
*
* @author Kai Kreuzer - Initial contribution
*/
public class BluetoothAddressTest {
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithNullParam() {
new BluetoothAddress(null);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorWithoutColons() {
new BluetoothAddress("123456789ABC");
}
@Test
public void testConstructorCorrect() {
new BluetoothAddress("12:34:56:78:9A:BC");
}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.ThingUID;
/**
* Mock implementation of a {@link BluetoothAdapter}.
*
* @author Connor Petty - Initial contribution
*/
@NonNullByDefault
public class MockBluetoothAdapter implements BluetoothAdapter {
private Map<BluetoothAddress, MockBluetoothDevice> devices = new ConcurrentHashMap<>();
private BluetoothAddress address = TestUtils.randomAddress();
private ThingUID uid = TestUtils.randomThingUID();
@Override
public ThingUID getUID() {
return uid;
}
@Override
public void addDiscoveryListener(BluetoothDiscoveryListener listener) {
}
@Override
public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) {
}
@Override
public void scanStart() {
}
@Override
public void scanStop() {
}
@Override
public @Nullable BluetoothAddress getAddress() {
return address;
}
@Override
public MockBluetoothDevice getDevice(BluetoothAddress address) {
return devices.computeIfAbsent(address, addr -> new MockBluetoothDevice(this, addr));
}
@Override
public boolean hasHandlerForDevice(BluetoothAddress address) {
return false;
}
@Override
public @Nullable String getLocation() {
return null;
}
@Override
public @Nullable String getLabel() {
return null;
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2020 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
/**
* Mock implementation of a {@link BluetoothDevice}.
*
* @author Connor Petty - Initial contribution
*/
@NonNullByDefault
public class MockBluetoothDevice extends BaseBluetoothDevice {
private AtomicBoolean servicesDiscovered = new AtomicBoolean(false);
/**
* This is the name that returned in the DEVICE_NAME characteristic
*/
private @Nullable String deviceName = null;
public MockBluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) {
super(adapter, address);
}
@Override
public boolean connect() {
this.connectionState = ConnectionState.CONNECTED;
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(ConnectionState.CONNECTED));
discoverServices();
return true;
}
@Override
public boolean discoverServices() {
if (!servicesDiscovered.getAndSet(true)) {
populateServices();
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
return true;
}
protected void populateServices() {
if (deviceName != null) {
BluetoothService service = new BluetoothService(BluetoothService.GattService.DEVICE_INFORMATION.getUUID());
service.addCharacteristic(new BluetoothCharacteristic(GattCharacteristic.DEVICE_NAME.getUUID(), 0));
addService(service);
}
}
@Override
public boolean disconnect() {
return true;
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
if (characteristic.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME) {
characteristic.setValue(deviceName);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
return true;
}
return false;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
@Override
protected void notifyListeners(BluetoothEventType event, Object... args) {
CompletableFuture.runAsync(() -> super.notifyListeners(event, args));
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
return false;
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
return false;
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
return false;
}
@Override
public boolean enableNotifications(BluetoothDescriptor descriptor) {
return false;
}
@Override
public boolean disableNotifications(BluetoothDescriptor descriptor) {
return false;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 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;
import org.apache.commons.lang.RandomStringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingUID;
/**
* Contains general utilities used for bluetooth tests
*
* @author Connor Petty - Initial contribution
*/
@NonNullByDefault
public class TestUtils {
public static BluetoothAddress randomAddress() {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 5; i++) {
builder.append(RandomStringUtils.random(2, "0123456789ABCDEF"));
builder.append(":");
}
builder.append(RandomStringUtils.random(2, "0123456789ABCDEF"));
return new BluetoothAddress(builder.toString());
}
public static ThingUID randomThingUID() {
return new ThingUID("mock", RandomStringUtils.randomAlphabetic(6));
}
}

View File

@@ -0,0 +1,586 @@
/**
* Copyright (c) 2010-2020 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.discovery.internal;
import static org.hamcrest.CoreMatchers.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import org.apache.commons.lang.RandomStringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
import org.openhab.binding.bluetooth.BluetoothDevice;
import org.openhab.binding.bluetooth.MockBluetoothAdapter;
import org.openhab.binding.bluetooth.MockBluetoothDevice;
import org.openhab.binding.bluetooth.TestUtils;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tests {@link BluetoothDiscoveryService}.
*
* @author Connor Petty - Initial contribution
*/
@NonNullByDefault
@RunWith(MockitoJUnitRunner.class)
public class BluetoothDiscoveryServiceTest {
private static final int TIMEOUT = 2000;
private final Logger logger = LoggerFactory.getLogger(BluetoothDiscoveryServiceTest.class);
private @NonNullByDefault({}) BluetoothDiscoveryService discoveryService;
@Spy
private @NonNullByDefault({}) MockDiscoveryParticipant participant1 = new MockDiscoveryParticipant();
@Mock
private @NonNullByDefault({}) DiscoveryListener mockDiscoveryListener;
@Before
public void setup() {
discoveryService = new BluetoothDiscoveryService();
discoveryService.addDiscoveryListener(mockDiscoveryListener);
discoveryService.addBluetoothDiscoveryParticipant(participant1);
}
@Test
public void ignoreDuplicateTest() {
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
// this second call should not produce another result
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void ignoreOtherDuplicateTest() {
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothAdapter mockAdapter2 = new MockBluetoothAdapter();
BluetoothAddress address = TestUtils.randomAddress();
BluetoothDevice device1 = mockAdapter1.getDevice(address);
BluetoothDevice device2 = mockAdapter2.getDevice(address);
discoveryService.deviceDiscovered(device1);
discoveryService.deviceDiscovered(device2);
// this should not produce another result
discoveryService.deviceDiscovered(device1);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void ignoreRssiDuplicateTest() {
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
// changing the rssi should not result in a new discovery
device.setRssi(100);
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void nonDuplicateNameTest() throws InterruptedException {
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
// this second call should produce another result
device.setName("sdfad");
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void nonDuplicateTxPowerTest() {
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
// this second call should produce another result
device.setTxPower(10);
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void nonDuplicateManufacturerIdTest() {
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
// this second call should produce another result
device.setManufacturerId(100);
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)));
}
@Test
public void useResultFromAnotherAdapterTest() {
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothAdapter mockAdapter2 = new MockBluetoothAdapter();
BluetoothAddress address = TestUtils.randomAddress();
discoveryService.deviceDiscovered(mockAdapter1.getDevice(address));
discoveryService.deviceDiscovered(mockAdapter2.getDevice(address));
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2))
.thingDiscovered(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
DiscoveryResult result1 = results.get(0);
DiscoveryResult result2 = results.get(1);
Assert.assertNotEquals(result1.getBridgeUID(), result2.getBridgeUID());
Assert.assertThat(result1.getBridgeUID(), anyOf(is(mockAdapter1.getUID()), is(mockAdapter2.getUID())));
Assert.assertThat(result2.getBridgeUID(), anyOf(is(mockAdapter1.getUID()), is(mockAdapter2.getUID())));
Assert.assertEquals(result1.getThingUID().getId(), result2.getThingUID().getId());
Assert.assertEquals(result1.getLabel(), result2.getLabel());
Assert.assertEquals(result1.getRepresentationProperty(), result2.getRepresentationProperty());
}
@Test
public void connectionParticipantTest() {
Mockito.doReturn(true).when(participant1).requiresConnection(ArgumentMatchers.any());
BluetoothAddress address = TestUtils.randomAddress();
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice mockDevice = mockAdapter1.getDevice(address);
String deviceName = RandomStringUtils.randomAlphanumeric(10);
mockDevice.setDeviceName(deviceName);
BluetoothDevice device = Mockito.spy(mockDevice);
discoveryService.deviceDiscovered(device);
Mockito.verify(device, Mockito.timeout(TIMEOUT).times(1)).connect();
Mockito.verify(device, Mockito.timeout(TIMEOUT).times(1)).readCharacteristic(
ArgumentMatchers.argThat(ch -> ch.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME));
Mockito.verify(device, Mockito.timeout(TIMEOUT).times(1)).disconnect();
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)
&& arg.getThingUID().getId().equals(deviceName)));
}
@Test
public void multiDiscoverySingleConnectionTest() {
Mockito.doReturn(true).when(participant1).requiresConnection(ArgumentMatchers.any());
BluetoothAddress address = TestUtils.randomAddress();
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothAdapter mockAdapter2 = new MockBluetoothAdapter();
MockBluetoothDevice mockDevice1 = mockAdapter1.getDevice(address);
MockBluetoothDevice mockDevice2 = mockAdapter2.getDevice(address);
String deviceName = RandomStringUtils.randomAlphanumeric(10);
mockDevice1.setDeviceName(deviceName);
mockDevice2.setDeviceName(deviceName);
BluetoothDevice device1 = Mockito.spy(mockDevice1);
BluetoothDevice device2 = Mockito.spy(mockDevice2);
discoveryService.deviceDiscovered(device1);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)
&& mockAdapter1.getUID().equals(arg.getBridgeUID())
&& arg.getThingUID().getId().equals(deviceName)));
Mockito.verify(device1, Mockito.times(1)).connect();
Mockito.verify(device1, Mockito.times(1)).readCharacteristic(
ArgumentMatchers.argThat(ch -> ch.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME));
Mockito.verify(device1, Mockito.times(1)).disconnect();
discoveryService.deviceDiscovered(device2);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)
&& mockAdapter2.getUID().equals(arg.getBridgeUID())
&& arg.getThingUID().getId().equals(deviceName)));
Mockito.verify(device2, Mockito.never()).connect();
Mockito.verify(device2, Mockito.never()).readCharacteristic(
ArgumentMatchers.argThat(ch -> ch.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME));
Mockito.verify(device2, Mockito.never()).disconnect();
}
@Test
public void nonConnectionParticipantTest() {
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice mockDevice = mockAdapter1.getDevice(TestUtils.randomAddress());
String deviceName = RandomStringUtils.randomAlphanumeric(10);
mockDevice.setDeviceName(deviceName);
BluetoothDevice device = Mockito.spy(mockDevice);
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant1.typeUID)
&& !arg.getThingUID().getId().equals(deviceName)));
Mockito.verify(device, Mockito.never()).connect();
Mockito.verify(device, Mockito.never()).readCharacteristic(
ArgumentMatchers.argThat(ch -> ch.getGattCharacteristic() == GattCharacteristic.DEVICE_NAME));
Mockito.verify(device, Mockito.never()).disconnect();
}
@Test
public void defaultResultTest() {
Mockito.doReturn(null).when(participant1).createResult(ArgumentMatchers.any());
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1))
.thingDiscovered(ArgumentMatchers.same(discoveryService), ArgumentMatchers
.argThat(arg -> arg.getThingTypeUID().equals(BluetoothBindingConstants.THING_TYPE_BEACON)));
}
@Test
public void removeDefaultDeviceTest() {
Mockito.doReturn(null).when(participant1).createResult(ArgumentMatchers.any());
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
discoveryService.deviceRemoved(device);
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1))
.thingDiscovered(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
DiscoveryResult result = resultCaptor.getValue();
Assert.assertEquals(BluetoothBindingConstants.THING_TYPE_BEACON, result.getThingTypeUID());
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingRemoved(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.equals(result.getThingUID())));
}
@Test
public void removeUpdatedDefaultDeviceTest() {
Mockito.doReturn(null).when(participant1).createResult(ArgumentMatchers.any());
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
device.setName("somename");
discoveryService.deviceDiscovered(device);
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2))
.thingDiscovered(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
DiscoveryResult result = resultCaptor.getValue();
Assert.assertEquals(BluetoothBindingConstants.THING_TYPE_BEACON, result.getThingTypeUID());
discoveryService.deviceRemoved(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingRemoved(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.equals(result.getThingUID())));
}
@Test
public void bluezConnectionTimeoutTest() {
Mockito.doReturn(true).when(participant1).requiresConnection(ArgumentMatchers.any());
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BadConnectionDevice device = new BadConnectionDevice(mockAdapter1, TestUtils.randomAddress(), 100);
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1))
.thingDiscovered(ArgumentMatchers.same(discoveryService), ArgumentMatchers
.argThat(arg -> arg.getThingTypeUID().equals(BluetoothBindingConstants.THING_TYPE_BEACON)));
}
@Test
public void replaceOlderDiscoveryTest() {
Mockito.doReturn(null).when(participant1).createResult(ArgumentMatchers.any());
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
MockDiscoveryParticipant participant2 = new MockDiscoveryParticipant() {
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
Integer manufacturer = device.getManufacturerId();
if (manufacturer != null && manufacturer.equals(10)) {
// without a device name it should produce a random ThingUID
return super.createResult(device);
}
return null;
}
};
discoveryService.addBluetoothDiscoveryParticipant(participant2);
// lets start with producing a default result
discoveryService.deviceDiscovered(device);
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1))
.thingDiscovered(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
DiscoveryResult result = resultCaptor.getValue();
Assert.assertEquals(BluetoothBindingConstants.THING_TYPE_BEACON, result.getThingTypeUID());
device.setManufacturerId(10);
// lets start with producing a default result
discoveryService.deviceDiscovered(device);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingRemoved(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.equals(result.getThingUID())));
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(1)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant2.typeUID)));
}
@Test
public void recursiveFutureTest() throws InterruptedException {
/*
* 1. deviceDiscovered(device1)
* 2. cause discovery to pause at participant1
* participant1 should make a field non-null for device1 upon unpause
* 3. make the same field non-null for device2
* 4. deviceDiscovered(device2)
* this discovery should be waiting for first discovery to finish
* 5. unpause participant
* End result:
* - participant should only have been called once
* - thingDiscovered should have been called twice
*/
Mockito.doReturn(null).when(participant1).createResult(ArgumentMatchers.any());
AtomicInteger callCount = new AtomicInteger(0);
final CountDownLatch pauseLatch = new CountDownLatch(1);
BluetoothAddress address = TestUtils.randomAddress();
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothAdapter mockAdapter2 = new MockBluetoothAdapter();
MockBluetoothDevice mockDevice1 = mockAdapter1.getDevice(address);
MockBluetoothDevice mockDevice2 = mockAdapter2.getDevice(address);
String deviceName = RandomStringUtils.randomAlphanumeric(10);
MockDiscoveryParticipant participant2 = new MockDiscoveryParticipant() {
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
try {
pauseLatch.await();
} catch (InterruptedException e) {
// do nothing
}
((BluetoothDeviceSnapshot) device).setName(deviceName);
callCount.incrementAndGet();
return super.createResult(device);
}
};
discoveryService.addBluetoothDiscoveryParticipant(participant2);
discoveryService.deviceDiscovered(mockDevice1);
mockDevice2.setName(deviceName);
discoveryService.deviceDiscovered(mockDevice2);
pauseLatch.countDown();
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2)).thingDiscovered(
ArgumentMatchers.same(discoveryService),
ArgumentMatchers.argThat(arg -> arg.getThingTypeUID().equals(participant2.typeUID)));
Assert.assertEquals(1, callCount.get());
}
@Test
public void roamingDiscoveryTest() {
RoamingDiscoveryParticipant roamingParticipant = new RoamingDiscoveryParticipant();
MockBluetoothAdapter roamingAdapter = roamingParticipant.roamingAdapter;
discoveryService.addBluetoothDiscoveryParticipant(roamingParticipant);
BluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
BluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
ArgumentCaptor<DiscoveryResult> resultCaptor = ArgumentCaptor.forClass(DiscoveryResult.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2))
.thingDiscovered(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
List<DiscoveryResult> results = resultCaptor.getAllValues();
DiscoveryResult result1 = results.get(0);
DiscoveryResult result2 = results.get(1);
Assert.assertNotEquals(result1.getBridgeUID(), result2.getBridgeUID());
Assert.assertThat(result1.getBridgeUID(), anyOf(is(mockAdapter1.getUID()), is(roamingAdapter.getUID())));
Assert.assertThat(result2.getBridgeUID(), anyOf(is(mockAdapter1.getUID()), is(roamingAdapter.getUID())));
Assert.assertEquals(result1.getThingUID().getId(), result2.getThingUID().getId());
Assert.assertEquals(result1.getLabel(), result2.getLabel());
Assert.assertEquals(result1.getRepresentationProperty(), result2.getRepresentationProperty());
}
@Test
public void roamingDiscoveryRetractionTest() {
RoamingDiscoveryParticipant roamingParticipant = new RoamingDiscoveryParticipant();
MockBluetoothAdapter roamingAdapter = roamingParticipant.roamingAdapter;
discoveryService.addBluetoothDiscoveryParticipant(roamingParticipant);
MockBluetoothAdapter mockAdapter1 = new MockBluetoothAdapter();
MockBluetoothDevice device = mockAdapter1.getDevice(TestUtils.randomAddress());
discoveryService.deviceDiscovered(device);
device.setName("dasf");
discoveryService.deviceDiscovered(device);
ArgumentCaptor<ThingUID> resultCaptor = ArgumentCaptor.forClass(ThingUID.class);
Mockito.verify(mockDiscoveryListener, Mockito.timeout(TIMEOUT).times(2))
.thingRemoved(ArgumentMatchers.same(discoveryService), resultCaptor.capture());
List<ThingUID> results = resultCaptor.getAllValues();
ThingUID result1 = results.get(0);
ThingUID result2 = results.get(1);
Assert.assertNotEquals(result1.getBridgeIds(), result2.getBridgeIds());
Assert.assertThat(result1.getBridgeIds().get(0),
anyOf(is(mockAdapter1.getUID().getId()), is(roamingAdapter.getUID().getId())));
Assert.assertThat(result2.getBridgeIds().get(0),
anyOf(is(mockAdapter1.getUID().getId()), is(roamingAdapter.getUID().getId())));
Assert.assertEquals(result1.getId(), result2.getId());
}
private class RoamingDiscoveryParticipant implements BluetoothDiscoveryParticipant {
private MockBluetoothAdapter roamingAdapter = new MockBluetoothAdapter();
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.emptySet();
}
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
return null;
}
@Override
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
return null;
}
@Override
public void publishAdditionalResults(DiscoveryResult result,
BiConsumer<BluetoothAdapter, DiscoveryResult> publisher) {
publisher.accept(roamingAdapter, result);
}
}
private class MockDiscoveryParticipant implements BluetoothDiscoveryParticipant {
private ThingTypeUID typeUID;
public MockDiscoveryParticipant() {
this.typeUID = new ThingTypeUID("mock", RandomStringUtils.randomAlphabetic(6));
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Collections.singleton(typeUID);
}
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
return DiscoveryResultBuilder.create(getThingUID(device)).withLabel(RandomStringUtils.randomAlphabetic(6))
.withRepresentationProperty(RandomStringUtils.randomAlphabetic(6))
.withBridge(device.getAdapter().getUID()).build();
}
@Override
public @NonNull ThingUID getThingUID(BluetoothDiscoveryDevice device) {
String id = device.getName() != null ? device.getName() : RandomStringUtils.randomAlphabetic(6);
return new ThingUID(typeUID, device.getAdapter().getUID(), id);
}
}
private class BadConnectionDevice extends MockBluetoothDevice {
private int sleepTime;
public BadConnectionDevice(BluetoothAdapter adapter, BluetoothAddress address, int sleepTime) {
super(adapter, address);
this.sleepTime = sleepTime;
}
@Override
public boolean connect() {
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(ConnectionState.CONNECTED));
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
// do nothing
}
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
return false;
}
}
}