added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 03:37:19 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.bluetooth</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,68 @@
# Bluetooth Binding overview
The Bluetooth binding is implemented to allow bundles to extend the main Bluetooth bundle (this one) in order to add new Bluetooth adapter as well as device support.
This architecture means that such extension bundles must utilise the binding name `bluetooth`.
A base class structure is defined in the `org.openhab.binding.bluetooth` bundle.
This includes the main classes required to implement Bluetooth:
* `BluetoothAdapter`. This interface defines the main functionality required to be implemented by a Bluetooth adapter, including device discovery. Typically, this interface is implemented by a BridgeHandler and then registered as an OSGi service
* `BluetoothDiscoveryParticipant`. An interface to be implemented by services that can identify specific Bluetooth devices.
* `BluetoothDevice`. This implements a Bluetooth device. It manages the notifications of device notifications, Bluetooth service and characteristic management, and provides the main interface to communicate to a Bluetooth device.
* `BluetoothService`. Implements the Bluetooth service. A service holds a number of characteristics.
* `BluetoothCharacteristic`. Implements the Bluetooth characteristic. This is the basic component for communicating data to and from a Bluetooth device.
* `BluetoothDescriptor`. Implements the Bluetooth descriptors for each characteristic.
## Implementing a new Bluetooth Adapter bundle
Bluetooth adapters are modelled as a bridge in openHAB.
The bridge handler provides the link with the Bluetooth hardware (eg a dongle, or system Bluetooth API).
An adapter bundle needs to implement two main classes: the `BridgeHandler` which should implement `BluetoothAdapter` (any be registered as a service), and a `ThingFactory`, which is required to instantiate the handler.
The bridge handler must implement any functionality required to interface to the Bluetooth layer.
It is responsible for managing the Bluetooth scanning, device discovery (i.e. the device interrogation to get the list of services and characteristics) and reading and writing of characteristics.
The bridge needs to manage any interaction between the interface with any things it provides – this needs to account for any constraints that an interface may impose such that things do not need to worry about any peculiarities imposed by a specific interface.
Classes such as `BluetoothCharacteristic` or `BluetoothService` may be extended to provide additional functionality to interface to a specific library if needed.
## Implementing specific Bluetooth device support
A specific Bluetooth thing handler provides the functionality required to interact with a specific Bluetooth device.
The new thing bundle needs to implement three main classes – a `BluetoothDiscoveryParticipant`, a `ThingHandler` and a `ThingFactory`, which is required to instantiate the handler.
Two fundamental communications methods can be employed in Bluetooth: beacons and connected mode. A Bluetooth thing handler can implement one or both of these communications
In practice, a connected mode Thing implementation would normally handle the beacons in order to provide as a minimum the RSSI data.
### Thing Naming
To avoid naming conflicts with different Bluetooth bundles a strict naming policy for things and thing xml files is proposed.
This should use the bundle name and the thing name, separated with an underscore - e.g. for a Yeelight binding Blue2 thing, the thing type would be `yeelight_blue2`.
### Connected Mode Implementation
The connected mode `BluetoothThingHandler` needs to handle the following functionality
* Extend the connected bluetooth thing handler. This holds the `adapter` through which all communication is done.
* Call the `adapter.getDevice()` method to get the `BluetoothDevice` class for the requested device. The `getDevice()` method will return a `BluetoothDevice` class even if the device is not currently known.
* Implement the `BluetoothDeviceListener` methods. These provide callbacks for various notifications regarding device updates – e.g. when the connection state of a device changes, when the device discovery is complete, when a read and write completes, and when beacon messages are received.
* The parent class calls the `device.connect()` method to connect to the device. Once the device is connected, the `BluetoothDeviceListener.onConnectionStateChange()` callback will be called.
* The parent class calls the `device.discoverServices()` method to discover all the BluetoothServices and `BluetoothCharacteristic`s implemented by the device. Once this is complete, the `BluetoothDeviceListener.onServicesDiscovered()` callback will be called.
* Call the `readCharacteristic` or `writeCharacteristic` methods to interact with the device. The `BluetoothDeviceListener.onCharacteristicReadComplete()` and `BluetoothDeviceListener.onCharacteristicWriteComplete()` methods will be called on completion.
* Implement the `BluetoothDeviceListener.onCharacteristicUpdate()` method to process any read responses or unsolicited updates of a characteristic value.
### Beacon Mode Implementation
The beacon mode thing handler needs to handle the following functionality:
* Extend the beacon Bluetooth thing handler. This holds the `adapter` through which all communication is done.
* Call the `adapter.getDevice()` method to get the `BluetoothDevice` class for the requested device. The `getDevice()` method will return a `BluetoothDevice` class even if the device is not currently known.
* Implement the `BluetoothDeviceListener.onScanRecordReceived()` method to process the beacons. The notification will provide the current receive signal strength (RSSI), the raw beacon data, and various elements of generally useful beacon data is provided separately.
### Generic Bluetooth Device Support
The core Bluetooth binding already includes generic "beacon" and "connected" Bluetooth thing types.
All devices for which no discovery participant defines a specific thing type are added to the inbox as a beacon device.
The corresponding handler implementation (`BeaconBluetoothHandler`) uses Beacon mode and merely defines a channel for RSSI for such devices.
The "connected" thing type can be used by manually defining a thing.
The corresponding handler implementation (`ConnectedBluetoothHandler`) uses Connected mode and thus immediately connects to the device and reads its services.
Common services are added as channels (t.b.d.).
@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
@@ -0,0 +1,80 @@
# Bluetooth Binding
This binding provides support for generic Bluetooth devices.
It has the following extensions:
<!--list-subs-->
## Bridges
In order to function, this binding requires a Bluetooth adapter to be present, which handles the wireless communication.
As there is no standard in Bluetooth for such dongles resp. chips, different adapters require a different implementation.
This is why the Bluetooth binding itself does not come with any bridge handlers for such adapters itself, but instead is extensible by additional bundles which can implement support for a specific adapter.
For Linux, there exists a special bundle which provides a Bluetooth bridge that talks to BlueZ.
This should be the best choice for any Linux-based single board computers like e.g. the Raspberry Pi.
## Supported Things
Two thing types are supported by this binding:
| Thing Type ID | Description |
|---------------|---------------------------------------------------------------------------------------------------------|
| beacon | A Bluetooth device that is not connected, but only broadcasts announcements. |
| connected | A Bluetooth device that allows a direct connection and which provides specific services when connected. |
## Discovery
Discovery is performed through the Bluetooth bridge.
Normally, any broadcasting Bluetooth device can be uniquely identified and thus a bridge can create an inbox result for it.
As this might lead to a huge list of devices, bridges usually also offer a way to deactivate this behavior.
## Thing Configuration
Both thing types only require a single configuration parameter `address`, which corresponds to the Bluetooth address of the device (in format "XX:XX:XX:XX:XX:XX").
## Channels
Every Bluetooth thing has the following channel:
| Channel ID | Item Type | Description |
|------------|-----------|-----------------------------------------------------------------------------------------------------|
| rssi | Number | The "Received Signal Strength Indicator", the [RSSI](https://blog.bluetooth.com/proximity-and-rssi) |
`connected` Things are dynamically queried for their services and if they support certain standard GATT characteristics, the appropriate channels are automatically added as well:
| Channel ID | Item Type | Description |
|---------------|-----------|-----------------------------------------------------------------|
| battery_level | Number | The device's battery level in percent |
## Full Example
demo.things (assuming you have a Bluetooth bridge with the ID `bluetooth:bluez:hci0`):
```
bluetooth:beacon:hci0:b1 "BLE Beacon" (bluetooth:bluez:hci0) [ address="68:64:4C:14:FC:C4" ]
```
demo.items:
```
Number Beacon_RSSI "My Beacon [%.0f]" { channel="bluetooth:beacon:hci0:b1:rssi" }
```
demo.sitemap:
```
sitemap demo label="Main Menu"
{
Frame {
Text item=Beacon_RSSI
}
}
```
See also the following extensions for further examples:
<!--list-subs-->
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.bluetooth</artifactId>
<name>openHAB Add-ons :: Bundles :: Bluetooth Binding</name>
</project>
@@ -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>
@@ -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;
}
}
@@ -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;
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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");
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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
}
@@ -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;
}
}
}
@@ -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);
}
}
}
}
@@ -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);
}
@@ -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);
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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();
}
}
@@ -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
}
}
@@ -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;
}
}
}
@@ -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) {
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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>
@@ -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>
@@ -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>
@@ -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");
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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;
}
}
}