[bluetooth.bluez] Complete Bluez rewrite (#8819)

Also-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
Signed-off-by: Benjamin Lafois <benjamin.lafois@gmail.com>
This commit is contained in:
Benjamin Lafois 2020-10-25 15:54:33 +01:00 committed by GitHub
parent b3d0f027cf
commit 72bf43cfa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1877 additions and 661 deletions

View File

@ -14,33 +14,33 @@ https://github.com/openhab/openhab-addons
== Third-party Content
TinyB Version: 0.5.1
BlueZ-DBus Version: 0.1.3
* License: MIT License
* Project: https://github.com/intel-iot-devkit/tinyb
* Source: https://github.com/intel-iot-devkit/tinyb/tree/v0.5.1
* Project: https://github.com/hypfvieh/bluez-dbus
* Source: https://github.com/hypfvieh/bluez-dbus
== Third-party license(s)
=== MIT License
The MIT License (MIT)
Copyright © 2015-2016 Intel Corporation
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Copyright (c) 2017 David M.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +1,6 @@
# Bluetooth BlueZ Adapter
This extension supports Bluetooth access via BlueZ on Linux (ARMv6hf).
This extension supports Bluetooth access via BlueZ and DBus on Linux. This is architecture agnostic and uses Unix Sockets.
# Setup
@ -44,14 +44,15 @@ It defines the following bridge type:
|----------------|---------------------------------------------------------------------------|
| bluez | A Bluetooth adapter that is supported by BlueZ |
## Discovery
If BlueZ is enabled and can be accessed, all available adapters are automatically discovered.
## Bridge Configuration
The bluez bridge requires the configuration parameter `address`, which corresponds to the Bluetooth address of the adapter (in format "XX:XX:XX:XX:XX:XX").
Additionally, the parameter `backgroundDiscovery` can be set to true/false.When set to true, any Bluetooth device of which broadcasts are received is added to the Inbox.
## Example

View File

@ -15,18 +15,21 @@
<name>openHAB Add-ons :: Bundles :: BlueZ Bluetooth Adapter</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bluetooth</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>intel-iot-devkit.tinyb</artifactId>
<version>0.5.1</version>
<scope>compile</scope>
<groupId>com.github.hypfvieh</groupId>
<artifactId>bluez-dbus-osgi</artifactId>
<version>0.1.3</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -4,7 +4,8 @@
<feature name="openhab-binding-bluetooth-bluez" description="Bluetooth Binding Bluez" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle dependency="true">mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version}</bundle>
</feature>

View File

@ -1,409 +0,0 @@
/**
* 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.bluez;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.bluetooth.BaseBluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothService;
import org.openhab.binding.bluetooth.bluez.handler.BlueZBridgeHandler;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tinyb.BluetoothException;
import tinyb.BluetoothGattCharacteristic;
import tinyb.BluetoothGattDescriptor;
import tinyb.BluetoothGattService;
/**
* Implementation of BluetoothDevice for BlueZ via TinyB
*
* @author Kai Kreuzer - Initial contribution and API
*
*/
public class BlueZBluetoothDevice extends BaseBluetoothDevice {
private tinyb.BluetoothDevice device;
private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
/**
* Constructor
*
* @param adapter the bridge handler through which this device is connected
* @param address the Bluetooth address of the device
* @param name the name of the device
*/
public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
super(adapter, address);
logger.debug("Creating BlueZ device with address '{}'", address);
}
/**
* Initializes a newly created instance of this class.
* This method should always be called directly after creating a new object instance.
*/
public void initialize() {
updateLastSeenTime();
}
/**
* Updates the internally used tinyB device instance. It replaces any previous instance, disables notifications on
* it and enables notifications on the new instance.
*
* @param tinybDevice the new device instance to use for communication
*/
public synchronized void updateTinybDevice(tinyb.BluetoothDevice tinybDevice) {
if (Objects.equals(device, tinybDevice)) {
return;
}
if (device != null) {
// we need to replace the instance - let's deactivate notifications on the old one
disableNotifications();
}
this.device = tinybDevice;
if (this.device == null) {
return;
}
updateLastSeenTime();
this.name = device.getName();
this.rssi = (int) device.getRSSI();
this.txPower = (int) device.getTxPower();
device.getManufacturerData().entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
.ifPresent(manufacturerId ->
// Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
this.manufacturer = manufacturerId & 0xFFFF);
if (device.getConnected()) {
this.connectionState = ConnectionState.CONNECTED;
}
enableNotifications();
refreshServices();
}
private void enableNotifications() {
logger.debug("Enabling notifications for device '{}'", device.getAddress());
device.enableRSSINotifications(n -> {
updateLastSeenTime();
rssi = (int) n;
BluetoothScanNotification notification = new BluetoothScanNotification();
notification.setRssi(n);
notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
});
device.enableManufacturerDataNotifications(n -> {
updateLastSeenTime();
for (Map.Entry<Short, byte[]> entry : n.entrySet()) {
BluetoothScanNotification notification = new BluetoothScanNotification();
byte[] data = new byte[entry.getValue().length + 2];
data[0] = (byte) (entry.getKey() & 0xFF);
data[1] = (byte) (entry.getKey() >>> 8);
System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
if (logger.isDebugEnabled()) {
logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
}
notification.setManufacturerData(data);
notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
}
});
device.enableConnectedNotifications(connected -> {
updateLastSeenTime();
connectionState = connected ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
logger.debug("Connection state of '{}' changed to {}", address, connectionState);
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(connectionState));
});
device.enableServicesResolvedNotifications(resolved -> {
updateLastSeenTime();
logger.debug("Received services resolved event for '{}': {}", address, resolved);
if (resolved) {
refreshServices();
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
});
device.enableServiceDataNotifications(data -> {
updateLastSeenTime();
if (logger.isDebugEnabled()) {
logger.debug("Received service data for '{}':", address);
for (Map.Entry<String, byte[]> entry : data.entrySet()) {
logger.debug("{} : {}", entry.getKey(), HexUtils.bytesToHex(entry.getValue(), " "));
}
}
});
}
private void disableNotifications() {
logger.debug("Disabling notifications for device '{}'", device.getAddress());
device.disableBlockedNotifications();
device.disableManufacturerDataNotifications();
device.disablePairedNotifications();
device.disableRSSINotifications();
device.disableServiceDataNotifications();
device.disableTrustedNotifications();
}
protected void refreshServices() {
if (device.getServices().size() > getServices().size()) {
for (BluetoothGattService tinybService : device.getServices()) {
BluetoothService service = new BluetoothService(UUID.fromString(tinybService.getUUID()),
tinybService.getPrimary());
for (BluetoothGattCharacteristic tinybCharacteristic : tinybService.getCharacteristics()) {
BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
UUID.fromString(tinybCharacteristic.getUUID()), 0);
for (BluetoothGattDescriptor tinybDescriptor : tinybCharacteristic.getDescriptors()) {
BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
UUID.fromString(tinybDescriptor.getUUID()));
characteristic.addDescriptor(descriptor);
}
service.addCharacteristic(characteristic);
}
addService(service);
}
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
}
@Override
public boolean connect() {
if (device != null && !device.getConnected()) {
try {
return device.connect();
} catch (BluetoothException e) {
if ("Timeout was reached".equals(e.getMessage())) {
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
} else if (e.getMessage() != null && e.getMessage().contains("Protocol not available")) {
// this device does not seem to be connectable at all - let's log a warning and ignore it.
logger.warn("Bluetooth device '{}' does not allow a connection.", device.getAddress());
} else {
logger.debug("Exception occurred when trying to connect device '{}': {}", device.getAddress(),
e.getMessage());
}
}
}
return false;
}
@Override
public boolean disconnect() {
if (device != null && device.getConnected()) {
logger.debug("Disconnecting '{}'", address);
try {
return device.disconnect();
} catch (BluetoothException e) {
logger.debug("Exception occurred when trying to disconnect device '{}': {}", device.getAddress(),
e.getMessage());
}
}
return false;
}
@Override
public boolean discoverServices() {
return false;
}
private void ensureConnected() {
if (device == null || !device.getConnected()) {
throw new IllegalStateException("TinyB device is not set or not connected");
}
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
scheduler.submit(() -> {
try {
byte[] value = c.readValue();
characteristic.setValue(value);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
} catch (BluetoothException e) {
logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
}
});
return true;
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
scheduler.submit(() -> {
try {
BluetoothCompletionStatus successStatus = c.writeValue(characteristic.getByteValue())
? BluetoothCompletionStatus.SUCCESS
: BluetoothCompletionStatus.ERROR;
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic, successStatus);
} catch (BluetoothException e) {
logger.debug("Exception occurred when trying to write characteristic '{}': {}",
characteristic.getUuid(), e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
}
});
return true;
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
try {
c.enableValueNotifications(value -> {
characteristic.setValue(value);
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
});
} catch (BluetoothException e) {
if (e.getMessage().contains("Already notifying")) {
return false;
} else if (e.getMessage().contains("In Progress")) {
// let's retry in 10 seconds
scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
}
}
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getTinybCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
c.disableValueNotifications();
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
}
@Override
public boolean enableNotifications(BluetoothDescriptor descriptor) {
ensureConnected();
BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
if (d != null) {
d.enableValueNotifications(value -> {
descriptor.setValue(value);
notifyListeners(BluetoothEventType.DESCRIPTOR_UPDATED, descriptor);
});
return true;
} else {
logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
return false;
}
}
@Override
public boolean disableNotifications(BluetoothDescriptor descriptor) {
ensureConnected();
BluetoothGattDescriptor d = getTinybDescriptorByUUID(descriptor.getUuid().toString());
if (d != null) {
d.disableValueNotifications();
return true;
} else {
logger.warn("Descriptor '{}' is missing on device '{}'.", descriptor.getUuid(), address);
return false;
}
}
private BluetoothGattCharacteristic getTinybCharacteristicByUUID(String uuid) {
for (BluetoothGattService service : device.getServices()) {
for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
if (c.getUUID().equals(uuid)) {
return c;
}
}
}
return null;
}
private BluetoothGattDescriptor getTinybDescriptorByUUID(String uuid) {
for (BluetoothGattService service : device.getServices()) {
for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
for (BluetoothGattDescriptor d : c.getDescriptors()) {
if (d.getUUID().equals(uuid)) {
return d;
}
}
}
}
return null;
}
/**
* Clean up and release memory.
*/
@Override
public void dispose() {
if (device == null) {
return;
}
disableNotifications();
try {
device.remove();
} catch (BluetoothException ex) {
if (ex.getMessage().contains("Does Not Exist")) {
// this happens when the underlying device has already been removed
// but we don't have a way to check if that is the case beforehand so
// we will just eat the error here.
} else {
logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
ex.getMessage());
}
}
}
}

View File

@ -1,172 +0,0 @@
/**
* 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.bluez.handler;
import java.util.List;
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.AbstractBluetoothBridgeHandler;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.bluez.BlueZBluetoothDevice;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tinyb.BluetoothException;
import tinyb.BluetoothManager;
/**
* The {@link BlueZBridgeHandler} is responsible for talking to the BlueZ stack.
* It provides a private interface for {@link BlueZBluetoothDevice}s to access the stack and provides top
* level adaptor functionality for scanning and arbitration.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Hilbrand Bouwkamp - Simplified calling scan and better handling manual scanning
* @author Connor Petty - Simplified device scan logic
*/
@NonNullByDefault
public class BlueZBridgeHandler extends AbstractBluetoothBridgeHandler<BlueZBluetoothDevice> {
private final Logger logger = LoggerFactory.getLogger(BlueZBridgeHandler.class);
private @NonNullByDefault({}) tinyb.BluetoothAdapter adapter;
// Our BT address
private @NonNullByDefault({}) BluetoothAddress adapterAddress;
private @NonNullByDefault({}) ScheduledFuture<?> discoveryJob;
/**
* Constructor
*
* @param bridge the bridge definition for this handler
*/
public BlueZBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
super.initialize();
BluetoothManager manager;
try {
manager = BluetoothManager.getBluetoothManager();
if (manager == null) {
throw new IllegalStateException("Received null BlueZ manager");
}
} catch (UnsatisfiedLinkError e) {
throw new IllegalStateException("BlueZ JNI connection cannot be established.", e);
} catch (RuntimeException e) {
// we do not get anything more specific from TinyB here
if (e.getMessage() != null && e.getMessage().contains("AccessDenied")) {
throw new IllegalStateException(
"Cannot access BlueZ stack due to permission problems. Make sure that your OS user is part of the 'bluetooth' group of BlueZ.");
} else {
throw new IllegalStateException("Cannot access BlueZ layer.", e);
}
}
final BlueZAdapterConfiguration configuration = getConfigAs(BlueZAdapterConfiguration.class);
if (configuration.address != null) {
adapterAddress = new BluetoothAddress(configuration.address);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "address not set");
return;
}
logger.debug("Creating BlueZ adapter with address '{}'", adapterAddress);
for (tinyb.BluetoothAdapter adapter : manager.getAdapters()) {
if (adapter == null) {
logger.warn("got null adapter from bluetooth manager");
continue;
}
if (adapter.getAddress().equals(adapterAddress.toString())) {
this.adapter = adapter;
discoveryJob = scheduler.scheduleWithFixedDelay(this::refreshDevices, 0, 10, TimeUnit.SECONDS);
return;
}
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No adapter for this address found.");
}
private void startDiscovery() {
// we need to make sure the adapter is powered first
if (!adapter.getPowered()) {
adapter.setPowered(true);
}
if (!adapter.getDiscovering()) {
adapter.setRssiDiscoveryFilter(-96);
adapter.startDiscovery();
}
}
private void refreshDevices() {
refreshTry: try {
logger.debug("Refreshing Bluetooth device list...");
List<tinyb.BluetoothDevice> tinybDevices = adapter.getDevices();
logger.debug("Found {} Bluetooth devices.", tinybDevices.size());
for (tinyb.BluetoothDevice tinybDevice : tinybDevices) {
BlueZBluetoothDevice device = getDevice(new BluetoothAddress(tinybDevice.getAddress()));
device.updateTinybDevice(tinybDevice);
deviceDiscovered(device);
}
// For whatever reason, bluez will sometimes turn off scanning. So we just make sure it keeps running.
startDiscovery();
} catch (BluetoothException ex) {
String message = ex.getMessage();
if (message != null) {
if (message.contains("Operation already in progress")) {
// we shouldn't go offline in this case
break refreshTry;
}
int idx = message.lastIndexOf(':');
if (idx != -1) {
message = message.substring(idx).trim();
}
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
return;
}
updateStatus(ThingStatus.ONLINE);
}
@Override
public @Nullable BluetoothAddress getAddress() {
return adapterAddress;
}
@Override
protected BlueZBluetoothDevice createDevice(BluetoothAddress address) {
BlueZBluetoothDevice device = new BlueZBluetoothDevice(this, address);
device.initialize();
return device;
}
@Override
public void dispose() {
if (discoveryJob != null) {
discoveryJob.cancel(true);
discoveryJob = null;
}
if (adapter != null && adapter.getDiscovering()) {
adapter.stopDiscovery();
}
super.dispose();
}
}

View File

@ -10,16 +10,19 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.bluez.handler;
package org.openhab.binding.bluetooth.bluez.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration;
/**
* Configuration properties class.
* Configuration properties for a bridge.
*
* @author Hilbrand Bouwkamp - Initial contribution
*/
@NonNullByDefault
public class BlueZAdapterConfiguration extends BaseBluetoothBridgeHandlerConfiguration {
public String address;
public @Nullable String address;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.bluez;
package org.openhab.binding.bluetooth.bluez.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
@ -30,4 +30,7 @@ public class BlueZAdapterConstants {
// Properties
public static final String PROPERTY_ADDRESS = "address";
private BlueZAdapterConstants() {
}
}

View File

@ -0,0 +1,467 @@
/**
* 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.bluez.internal;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.bluez.exceptions.BluezFailedException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.freedesktop.dbus.errors.NoReply;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.types.UInt16;
import org.openhab.binding.bluetooth.BaseBluetoothDevice;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDescriptor;
import org.openhab.binding.bluetooth.BluetoothService;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
import org.openhab.binding.bluetooth.bluez.internal.events.CharacteristicUpdateEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattCharacteristic;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattDescriptor;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothGattService;
/**
* Implementation of BluetoothDevice for BlueZ via DBus-BlueZ API
*
* @author Kai Kreuzer - Initial contribution and API
* @author Benjamin Lafois - Replaced tinyB with bluezDbus
*
*/
@NonNullByDefault
public class BlueZBluetoothDevice extends BaseBluetoothDevice implements BlueZEventListener {
private final Logger logger = LoggerFactory.getLogger(BlueZBluetoothDevice.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
// Device from native lib
private @Nullable BluetoothDevice device = null;
/**
* Constructor
*
* @param adapter the bridge handler through which this device is connected
* @param address the Bluetooth address of the device
* @param name the name of the device
*/
public BlueZBluetoothDevice(BlueZBridgeHandler adapter, BluetoothAddress address) {
super(adapter, address);
logger.debug("Creating DBusBlueZ device with address '{}'", address);
}
public synchronized void updateBlueZDevice(@Nullable BluetoothDevice blueZDevice) {
if (this.device != null && this.device == blueZDevice) {
return;
}
logger.debug("updateBlueZDevice({})", blueZDevice);
this.device = blueZDevice;
if (blueZDevice == null) {
return;
}
Short rssi = blueZDevice.getRssi();
if (rssi != null) {
this.rssi = rssi.intValue();
}
this.name = blueZDevice.getName();
Map<UInt16, byte[]> manData = blueZDevice.getManufacturerData();
if (manData != null) {
manData.entrySet().stream().map(Map.Entry::getKey).filter(Objects::nonNull).findFirst()
.ifPresent((UInt16 manufacturerId) ->
// Convert to unsigned int to match the convention in BluetoothCompanyIdentifiers
this.manufacturer = manufacturerId.intValue() & 0xFFFF);
}
if (Boolean.TRUE.equals(blueZDevice.isConnected())) {
setConnectionState(ConnectionState.CONNECTED);
}
discoverServices();
}
/**
* Clean up and release memory.
*/
@Override
public void dispose() {
BluetoothDevice dev = device;
if (dev != null) {
try {
dev.getAdapter().removeDevice(dev.getRawDevice());
} catch (DBusException ex) {
if (ex.getMessage().contains("Does Not Exist")) {
// this happens when the underlying device has already been removed
// but we don't have a way to check if that is the case beforehand so
// we will just eat the error here.
} else {
logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
ex.getMessage());
}
} catch (RuntimeException ex) {
// try to catch any other exceptions
logger.debug("Exception occurred when trying to remove inactive device '{}': {}", address,
ex.getMessage());
}
}
}
private void setConnectionState(ConnectionState state) {
if (this.connectionState != state) {
this.connectionState = state;
notifyListeners(BluetoothEventType.CONNECTION_STATE, new BluetoothConnectionStatusNotification(state));
}
}
@Override
public boolean connect() {
logger.debug("Connect({})", device);
BluetoothDevice dev = device;
if (dev != null) {
if (Boolean.FALSE.equals(dev.isConnected())) {
try {
boolean ret = dev.connect();
logger.debug("Connect result: {}", ret);
return ret;
} catch (NoReply e) {
// Have to double check because sometimes, exception but still worked
logger.debug("Got a timeout - but sometimes happen. Is Connected ? {}", dev.isConnected());
if (Boolean.FALSE.equals(dev.isConnected())) {
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(ConnectionState.DISCONNECTED));
return false;
} else {
return true;
}
} catch (DBusExecutionException e) {
// Catch "software caused connection abort"
return false;
} catch (Exception e) {
logger.warn("error occured while trying to connect", e);
}
} else {
logger.debug("Device was already connected");
// we might be stuck in another state atm so we need to trigger a connected in this case
setConnectionState(ConnectionState.CONNECTED);
return true;
}
}
return false;
}
@Override
public boolean disconnect() {
BluetoothDevice dev = device;
if (dev != null) {
logger.debug("Disconnecting '{}'", address);
return dev.disconnect();
}
return false;
}
private void ensureConnected() {
BluetoothDevice dev = device;
if (dev == null || !dev.isConnected()) {
throw new IllegalStateException("DBusBlueZ device is not set or not connected");
}
}
private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByUUID(String uuid) {
BluetoothDevice dev = device;
if (dev == null) {
return null;
}
for (BluetoothGattService service : dev.getGattServices()) {
for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
if (c.getUuid().equalsIgnoreCase(uuid)) {
return c;
}
}
}
return null;
}
private @Nullable BluetoothGattCharacteristic getDBusBlueZCharacteristicByDBusPath(String dBusPath) {
BluetoothDevice dev = device;
if (dev == null) {
return null;
}
for (BluetoothGattService service : dev.getGattServices()) {
if (dBusPath.startsWith(service.getDbusPath())) {
for (BluetoothGattCharacteristic characteristic : service.getGattCharacteristics()) {
if (dBusPath.startsWith(characteristic.getDbusPath())) {
return characteristic;
}
}
}
}
return null;
}
private @Nullable BluetoothGattDescriptor getDBusBlueZDescriptorByUUID(String uuid) {
BluetoothDevice dev = device;
if (dev == null) {
return null;
}
for (BluetoothGattService service : dev.getGattServices()) {
for (BluetoothGattCharacteristic c : service.getGattCharacteristics()) {
for (BluetoothGattDescriptor d : c.getGattDescriptors()) {
if (d.getUuid().equalsIgnoreCase(uuid)) {
return d;
}
}
}
}
return null;
}
@Override
public boolean enableNotifications(BluetoothCharacteristic characteristic) {
ensureConnected();
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
try {
c.startNotify();
} catch (DBusException e) {
if (e.getMessage().contains("Already notifying")) {
return false;
} else if (e.getMessage().contains("In Progress")) {
// let's retry in 10 seconds
scheduler.schedule(() -> enableNotifications(characteristic), 10, TimeUnit.SECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
}
}
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
}
@Override
public boolean writeCharacteristic(BluetoothCharacteristic characteristic) {
logger.debug("writeCharacteristic()");
ensureConnected();
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
scheduler.submit(() -> {
try {
c.writeValue(characteristic.getByteValue(), null);
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
} catch (DBusException e) {
logger.debug("Exception occurred when trying to write characteristic '{}': {}",
characteristic.getUuid(), e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
}
});
return true;
}
@Override
public void onDBusBlueZEvent(BlueZEvent event) {
logger.debug("Unsupported event: {}", event);
}
@Override
public void onServicesResolved(ServicesResolvedEvent event) {
if (event.isResolved()) {
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
}
@Override
public void onNameUpdate(NameEvent event) {
BluetoothScanNotification notification = new BluetoothScanNotification();
notification.setDeviceName(event.getName());
notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
}
@Override
public void onManufacturerDataUpdate(ManufacturerDataEvent event) {
for (Map.Entry<Short, byte[]> entry : event.getData().entrySet()) {
BluetoothScanNotification notification = new BluetoothScanNotification();
byte[] data = new byte[entry.getValue().length + 2];
data[0] = (byte) (entry.getKey() & 0xFF);
data[1] = (byte) (entry.getKey() >>> 8);
System.arraycopy(entry.getValue(), 0, data, 2, entry.getValue().length);
if (logger.isDebugEnabled()) {
logger.debug("Received manufacturer data for '{}': {}", address, HexUtils.bytesToHex(data, " "));
}
notification.setManufacturerData(data);
notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
}
}
@Override
public void onTxPowerUpdate(TXPowerEvent event) {
this.txPower = (int) event.getTxPower();
}
@Override
public void onCharacteristicNotify(CharacteristicUpdateEvent event) {
// Here it is a bit special - as the event is linked to the DBUS path, not characteristic UUID.
// So we need to find the characteristic by its DBUS path.
BluetoothGattCharacteristic characteristic = getDBusBlueZCharacteristicByDBusPath(event.getDbusPath());
if (characteristic == null) {
logger.debug("Received a notification for a characteristic not found on device.");
return;
}
BluetoothCharacteristic c = getCharacteristic(UUID.fromString(characteristic.getUuid()));
if (c != null) {
c.setValue(event.getData());
notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, c, BluetoothCompletionStatus.SUCCESS);
}
}
@Override
public void onRssiUpdate(RssiEvent event) {
int rssiTmp = event.getRssi();
this.rssi = rssiTmp;
BluetoothScanNotification notification = new BluetoothScanNotification();
notification.setRssi(rssiTmp);
notifyListeners(BluetoothEventType.SCAN_RECORD, notification);
}
@Override
public void onConnectedStatusUpdate(ConnectedEvent event) {
this.connectionState = event.isConnected() ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED;
notifyListeners(BluetoothEventType.CONNECTION_STATE,
new BluetoothConnectionStatusNotification(connectionState));
}
@Override
public boolean discoverServices() {
BluetoothDevice dev = device;
if (dev == null) {
return false;
}
if (dev.getGattServices().size() > getServices().size()) {
for (BluetoothGattService dBusBlueZService : dev.getGattServices()) {
BluetoothService service = new BluetoothService(UUID.fromString(dBusBlueZService.getUuid()),
dBusBlueZService.isPrimary());
for (BluetoothGattCharacteristic dBusBlueZCharacteristic : dBusBlueZService.getGattCharacteristics()) {
BluetoothCharacteristic characteristic = new BluetoothCharacteristic(
UUID.fromString(dBusBlueZCharacteristic.getUuid()), 0);
for (BluetoothGattDescriptor dBusBlueZDescriptor : dBusBlueZCharacteristic.getGattDescriptors()) {
BluetoothDescriptor descriptor = new BluetoothDescriptor(characteristic,
UUID.fromString(dBusBlueZDescriptor.getUuid()));
characteristic.addDescriptor(descriptor);
}
service.addCharacteristic(characteristic);
}
addService(service);
}
notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
}
return true;
}
@Override
public boolean readCharacteristic(BluetoothCharacteristic characteristic) {
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c == null) {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
scheduler.submit(() -> {
try {
byte[] value = c.readValue(null);
characteristic.setValue(value);
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.SUCCESS);
} catch (DBusException e) {
logger.debug("Exception occurred when trying to read characteristic '{}': {}", characteristic.getUuid(),
e.getMessage());
notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
BluetoothCompletionStatus.ERROR);
}
});
return true;
}
@Override
public boolean disableNotifications(BluetoothCharacteristic characteristic) {
BluetoothGattCharacteristic c = getDBusBlueZCharacteristicByUUID(characteristic.getUuid().toString());
if (c != null) {
try {
c.stopNotify();
} catch (BluezFailedException e) {
if (e.getMessage().contains("In Progress")) {
// let's retry in 10 seconds
scheduler.schedule(() -> disableNotifications(characteristic), 10, TimeUnit.SECONDS);
} else {
logger.warn("Exception occurred while activating notifications on '{}'", address, e);
}
}
return true;
} else {
logger.warn("Characteristic '{}' is missing on device '{}'.", characteristic.getUuid(), address);
return false;
}
}
@Override
public boolean enableNotifications(BluetoothDescriptor descriptor) {
// Not sure if it is possible to implement this
return false;
}
@Override
public boolean disableNotifications(BluetoothDescriptor descriptor) {
// Not sure if it is possible to implement this
return false;
}
}

View File

@ -0,0 +1,234 @@
/**
* 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.bluez.internal;
import java.util.List;
import java.util.concurrent.Future;
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.AbstractBluetoothBridgeHandler;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.bluez.internal.events.AdapterDiscoveringChangedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.AdapterPoweredChangedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
/**
* The {@link BlueZBridgeHandler} is responsible for talking to the BlueZ stack, using DBus Unix Socket.
* This Binding does not use any JNI.
* It provides a private interface for {@link BlueZBluetoothDevice}s to access the stack and provides top
* level adaptor functionality for scanning and arbitration.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Hilbrand Bouwkamp - Simplified calling scan and better handling manual scanning
* @author Connor Petty - Simplified device scan logic
* @author Benjamin Lafois - Replaced tinyB with bluezDbus
*
*/
@NonNullByDefault
public class BlueZBridgeHandler extends AbstractBluetoothBridgeHandler<BlueZBluetoothDevice>
implements BlueZEventListener {
private final Logger logger = LoggerFactory.getLogger(BlueZBridgeHandler.class);
// ADAPTER from BlueZ-DBus Library
private @Nullable BluetoothAdapter adapter;
// Our BT address
private @Nullable BluetoothAddress adapterAddress;
private @Nullable ScheduledFuture<?> discoveryJob;
private final DeviceManagerFactory deviceManagerFactory;
/**
* Constructor
*
* @param bridge the bridge definition for this handler
*/
public BlueZBridgeHandler(Bridge bridge, DeviceManagerFactory deviceManagerFactory) {
super(bridge);
this.deviceManagerFactory = deviceManagerFactory;
}
@Override
public void initialize() {
super.initialize();
// Load configuration
final BlueZAdapterConfiguration configuration = getConfigAs(BlueZAdapterConfiguration.class);
String addr = configuration.address;
if (addr != null) {
this.adapterAddress = new BluetoothAddress(addr.toUpperCase());
} else {
// If configuration does not contain adapter address to use, exit with error.
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "address not set");
return;
}
logger.debug("Creating BlueZ adapter with address '{}'", adapterAddress);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Initializing");
deviceManagerFactory.getPropertiesChangedHandler().addListener(this);
discoveryJob = scheduler.scheduleWithFixedDelay(this::initializeAndRefreshDevices, 5, 10, TimeUnit.SECONDS);
}
@Override
public void dispose() {
deviceManagerFactory.getPropertiesChangedHandler().removeListener(this);
logger.debug("Termination of DBus BlueZ handler");
Future<?> job = discoveryJob;
if (job != null) {
job.cancel(false);
discoveryJob = null;
}
BluetoothAdapter localAdatper = this.adapter;
if (localAdatper != null) {
localAdatper.stopDiscovery();
this.adapter = null;
}
super.dispose();
}
private @Nullable BluetoothAdapter prepareAdapter(DeviceManagerWrapper deviceManager) {
// next lets check if we can find our adapter in the manager.
BluetoothAdapter localAdapter = adapter;
if (localAdapter == null) {
BluetoothAddress localAddress = adapterAddress;
if (localAddress != null) {
localAdapter = adapter = deviceManager.getAdapter(localAddress);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No adapter address provided");
return null;
}
}
if (localAdapter == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Native adapter could not be found for address '" + adapterAddress + "'");
return null;
}
// now lets confirm that the adapter is powered
if (!localAdapter.isPowered()) {
localAdapter.setPowered(true);
// give the device some time to power on
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
"Adapter is not powered, attempting to turn on...");
return null;
}
// now lets make sure that discovery is turned on
if (!localAdapter.startDiscovery()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Trying to start discovery");
return null;
}
return localAdapter;
}
private void initializeAndRefreshDevices() {
logger.debug("initializeAndRefreshDevice()");
try {
// first check if the device manager is ready
DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager();
if (deviceManager == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Bluez DeviceManager not available yet.");
return;
}
BluetoothAdapter adapter = prepareAdapter(deviceManager);
if (adapter == null) {
// adapter isn't prepared yet
return;
}
// now lets refresh devices
List<BluetoothDevice> bluezDevices = deviceManager.getDevices(adapter);
logger.debug("Found {} Bluetooth devices.", bluezDevices.size());
for (BluetoothDevice bluezDevice : bluezDevices) {
if (bluezDevice.getAddress() == null) {
// For some reasons, sometimes the address is null..
continue;
}
BlueZBluetoothDevice device = getDevice(new BluetoothAddress(bluezDevice.getAddress()));
device.updateBlueZDevice(bluezDevice);
deviceDiscovered(device);
}
updateStatus(ThingStatus.ONLINE);
} catch (Exception ex) {
// don't know what kind of exception the bluez library might throw at us so lets catch them here so our
// scheduler loop doesn't get terminated
logger.warn("Unknown exception", ex);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
}
}
@Override
public @Nullable BluetoothAddress getAddress() {
return adapterAddress;
}
@Override
protected BlueZBluetoothDevice createDevice(BluetoothAddress address) {
logger.debug("createDevice {}", address);
BlueZBluetoothDevice device = new BlueZBluetoothDevice(this, address);
return device;
}
@Override
public void onDBusBlueZEvent(BlueZEvent event) {
BluetoothAdapter localAdapter = this.adapter;
String adapterName = event.getAdapterName();
if (adapterName == null || localAdapter == null) {
// We cannot be sure that this event concerns this adapter.. So ignore message
return;
}
String localName = localAdapter.getDeviceName();
if (!adapterName.equals(localName)) {
// does not concern this adapter
return;
}
BluetoothAddress address = event.getDevice();
if (address != null) {
// now lets forward the event to the corresponding bluetooth device
BlueZBluetoothDevice device = getDevice(address);
event.dispatch(device);
}
}
@Override
public void onDiscoveringChanged(AdapterDiscoveringChangedEvent event) {
// do nothing for now
}
@Override
public void onPoweredChange(AdapterPoweredChangedEvent event) {
// do nothing for now
}
}

View File

@ -10,22 +10,26 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.bluez.internal.discovery;
package org.openhab.binding.bluetooth.bluez.internal;
import java.util.Collections;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.bluetooth.bluez.BlueZAdapterConstants;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tinyb.BluetoothAdapter;
import tinyb.BluetoothManager;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter;
/**
* This is a discovery service, which checks whether we are running on a Linux with a BlueZ stack.
@ -33,41 +37,59 @@ import tinyb.BluetoothManager;
*
* @author Kai Kreuzer - Initial Contribution and API
* @author Hilbrand Bouwkamp - Moved background scan to actual background method
* @author Connor Petty - Replaced tinyB with bluezDbus
*
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.bluetooth.bluez")
public class BlueZDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(BlueZDiscoveryService.class);
private BluetoothManager manager;
private final DeviceManagerFactory deviceManagerFactory;
private @Nullable Future<?> backgroundScan;
public BlueZDiscoveryService() {
@Activate
public BlueZDiscoveryService(@Reference DeviceManagerFactory deviceManagerFactory) {
super(Collections.singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ), 1, true);
this.deviceManagerFactory = deviceManagerFactory;
}
private static void cancel(@Nullable Future<?> future) {
if (future != null) {
future.cancel(false);
}
}
@Override
protected void startBackgroundDiscovery() {
startScan();
backgroundScan = scheduler.scheduleWithFixedDelay(() -> {
DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager();
if (deviceManager == null) {
return;
}
startScan();
}, 5, 10, TimeUnit.SECONDS);
}
@Override
protected void stopBackgroundDiscovery() {
cancel(backgroundScan);
backgroundScan = null;
}
@Override
protected void startScan() {
try {
manager = BluetoothManager.getBluetoothManager();
manager.getAdapters().stream().map(this::createDiscoveryResult).forEach(this::thingDiscovered);
} catch (UnsatisfiedLinkError e) {
logger.debug("Not possible to initialize the BlueZ stack. ", e);
DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager();
if (deviceManager == null) {
logger.warn("The DeviceManager is not available");
return;
} catch (RuntimeException e) {
// we do not get anything more specific from TinyB here
if (e.getMessage() != null && e.getMessage().contains("AccessDenied")) {
logger.warn(
"Cannot access BlueZ stack due to permission problems. Make sure that your OS user is part of the 'bluetooth' group of BlueZ.");
} else {
logger.warn("Failed to scan for Bluetooth devices", e);
}
}
// the first time the device manager is not null we can cancel background discovery
stopBackgroundDiscovery();
deviceManager.scanForBluetoothAdapters().stream()//
.map(this::createDiscoveryResult)//
.forEach(this::thingDiscovered);
}
private DiscoveryResult createDiscoveryResult(BluetoothAdapter adapter) {
@ -78,6 +100,6 @@ public class BlueZDiscoveryService extends AbstractDiscoveryService {
}
private String getId(BluetoothAdapter adapter) {
return adapter.getInterfaceName().replaceAll("[^a-zA-Z0-9_]", "");
return adapter.getDeviceName().replaceAll("[^a-zA-Z0-9_]", "");
}
}

View File

@ -18,9 +18,9 @@ import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAdapter;
import org.openhab.binding.bluetooth.bluez.BlueZAdapterConstants;
import org.openhab.binding.bluetooth.bluez.handler.BlueZBridgeHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@ -30,21 +30,32 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link BlueZHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Kai Kreuzer - Initial contribution and API
* @author Connor Petty - Added DeviceManagerFactory
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.bluetooth.bluez")
public class BlueZHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ);
private final Map<ThingUID, ServiceRegistration<?>> serviceRegs = new HashMap<>();
private final Map<ThingUID, @Nullable ServiceRegistration<?>> serviceRegs = new HashMap<>();
private final DeviceManagerFactory deviceManagerFactory;
@Activate
public BlueZHandlerFactory(@Reference DeviceManagerFactory deviceManagerFactory) {
this.deviceManagerFactory = deviceManagerFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@ -52,11 +63,11 @@ public class BlueZHandlerFactory extends BaseThingHandlerFactory {
}
@Override
protected ThingHandler createHandler(Thing thing) {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(BlueZAdapterConstants.THING_TYPE_BLUEZ)) {
BlueZBridgeHandler handler = new BlueZBridgeHandler((Bridge) thing);
BlueZBridgeHandler handler = new BlueZBridgeHandler((Bridge) thing, deviceManagerFactory);
registerBluetoothAdapter(handler);
return handler;
} else {

View File

@ -0,0 +1,212 @@
/**
* 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.bluez.internal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.freedesktop.dbus.DBusMap;
import org.freedesktop.dbus.handlers.AbstractPropertiesChangedHandler;
import org.freedesktop.dbus.interfaces.Properties.PropertiesChanged;
import org.freedesktop.dbus.types.UInt16;
import org.freedesktop.dbus.types.Variant;
import org.openhab.binding.bluetooth.bluez.internal.events.AdapterDiscoveringChangedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.AdapterPoweredChangedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
import org.openhab.binding.bluetooth.bluez.internal.events.CharacteristicUpdateEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ConnectedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ManufacturerDataEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.NameEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.RssiEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.ServicesResolvedEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.TXPowerEvent;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the PropertiesChangedHandler subclass used by the binding to handle/dispatch property change events
* from bluez.
*
* @author Benjamin Lafois - Initial contribution and API
* @author Connor Petty - Code cleanup
*/
@NonNullByDefault
public class BlueZPropertiesChangedHandler extends AbstractPropertiesChangedHandler {
private final Logger logger = LoggerFactory.getLogger(BlueZPropertiesChangedHandler.class);
private final Set<BlueZEventListener> listeners = new CopyOnWriteArraySet<>();
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
public void addListener(BlueZEventListener listener) {
this.listeners.add(listener);
}
public void removeListener(BlueZEventListener listener) {
this.listeners.remove(listener);
}
private void notifyListeners(BlueZEvent event) {
for (BlueZEventListener listener : this.listeners) {
event.dispatch(listener);
}
}
@Override
public void handle(@Nullable PropertiesChanged properties) {
if (properties == null || properties.getPropertiesChanged() == null) {
logger.debug("Null properties. Skipping.");
return;
}
Map<@Nullable String, @Nullable Variant<?>> changedProperties = properties.getPropertiesChanged();
if (changedProperties == null) {
logger.debug("Null properties changed. Skipping.");
return;
}
// do this asynchronously so that we don't slow things down for the dbus event dispatcher
scheduler.execute(() -> {
String dbusPath = properties.getPath();
changedProperties.forEach((key, variant) -> {
if (key == null || variant == null) {
return;
}
switch (key.toLowerCase()) {
case "rssi":
// Signal Update
onRSSIUpdate(dbusPath, variant);
break;
case "txpower":
// TxPower
onTXPowerUpdate(dbusPath, variant);
break;
case "value":
// Characteristc value updated
onValueUpdate(dbusPath, variant);
break;
case "connected":
onConnectedUpdate(dbusPath, variant);
break;
case "name":
onNameUpdate(dbusPath, variant);
break;
case "alias":
// TODO
break;
case "manufacturerdata":
onManufacturerDataUpdate(dbusPath, variant);
break;
case "powered":
onPoweredUpdate(dbusPath, variant);
break;
case "discovering":
onDiscoveringUpdate(dbusPath, variant);
break;
case "servicesresolved":
onServicesResolved(dbusPath, variant);
break;
}
});
logger.debug("PropertiesPath: {}", dbusPath);
logger.debug("PropertiesChanged: {}", changedProperties);
});
}
private void onDiscoveringUpdate(String dbusPath, Variant<?> variant) {
Object discovered = variant.getValue();
if (discovered instanceof Boolean) {
notifyListeners(new AdapterDiscoveringChangedEvent(dbusPath, (boolean) discovered));
}
}
private void onPoweredUpdate(String dbusPath, Variant<?> variant) {
Object powered = variant.getValue();
if (powered instanceof Boolean) {
notifyListeners(new AdapterPoweredChangedEvent(dbusPath, (boolean) powered));
}
}
private void onServicesResolved(String dbusPath, Variant<?> variant) {
Object resolved = variant.getValue();
if (resolved instanceof Boolean) {
notifyListeners(new ServicesResolvedEvent(dbusPath, (boolean) resolved));
}
}
private void onNameUpdate(String dbusPath, Variant<?> variant) {
Object name = variant.getValue();
if (name instanceof String) {
notifyListeners(new NameEvent(dbusPath, (String) name));
}
}
private void onTXPowerUpdate(String dbusPath, Variant<?> variant) {
Object txPower = variant.getValue();
if (txPower instanceof Short) {
notifyListeners(new TXPowerEvent(dbusPath, (short) txPower));
}
}
private void onConnectedUpdate(String dbusPath, Variant<?> variant) {
Object connected = variant.getValue();
if (connected instanceof Boolean) {
notifyListeners(new ConnectedEvent(dbusPath, (boolean) connected));
}
}
private void onManufacturerDataUpdate(String dbusPath, Variant<?> variant) {
Map<Short, byte[]> eventData = new HashMap<>();
Object map = variant.getValue();
if (map instanceof DBusMap) {
DBusMap<?, ?> dbm = (DBusMap<?, ?>) map;
for (Map.Entry<?, ?> entry : dbm.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (key instanceof UInt16 && value instanceof Variant<?>) {
value = ((Variant<?>) value).getValue();
if (value instanceof byte[]) {
eventData.put(((UInt16) key).shortValue(), ((byte[]) value));
}
}
}
}
if (!eventData.isEmpty()) {
notifyListeners(new ManufacturerDataEvent(dbusPath, eventData));
}
}
private void onValueUpdate(String dbusPath, Variant<?> variant) {
Object value = variant.getValue();
if (value instanceof byte[]) {
notifyListeners(new CharacteristicUpdateEvent(dbusPath, (byte[]) value));
}
}
private void onRSSIUpdate(String dbusPath, Variant<?> variant) {
Object rssi = variant.getValue();
if (rssi instanceof Short) {
notifyListeners(new RssiEvent(dbusPath, (short) rssi));
}
}
}

View File

@ -0,0 +1,186 @@
/**
* 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.bluez.internal;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.freedesktop.dbus.exceptions.DBusException;
import org.openhab.core.common.ThreadPoolManager;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.hypfvieh.bluetooth.DeviceManager;
/**
* This service handles the lifecycle of the {@link DeviceManager} singleton instance.
* In addition, this class is responsible for managing the BlueZPropertiesChangedHandler instance
* used by the binding for listening and dispatching dbus events from the DeviceManager.
*
* Creation of the DeviceManagerWrapper is asynchronous and thus attempts to retrieve the
* DeviceManagerWrapper through 'getDeviceManager' may initially fail.
*
* @author Connor Petty - Initial Contribution
*
*/
@NonNullByDefault
@Component(service = DeviceManagerFactory.class)
public class DeviceManagerFactory {
private final Logger logger = LoggerFactory.getLogger(DeviceManagerFactory.class);
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
private final BlueZPropertiesChangedHandler changeHandler = new BlueZPropertiesChangedHandler();
private @Nullable CompletableFuture<DeviceManager> deviceManagerFuture;
private @Nullable CompletableFuture<DeviceManagerWrapper> deviceManagerWrapperFuture;
public BlueZPropertiesChangedHandler getPropertiesChangedHandler() {
return changeHandler;
}
public @Nullable DeviceManagerWrapper getDeviceManager() {
// we can cheat the null checker with casting here
var future = (CompletableFuture<@Nullable DeviceManagerWrapper>) deviceManagerWrapperFuture;
if (future != null) {
return future.getNow(null);
}
return null;
}
@Activate
public void initialize() {
logger.debug("initializing DeviceManagerFactory");
var stage1 = this.deviceManagerFuture = callAsync(() -> {
try {
// if this is the first call to the library, this call
// should throw an exception (that we are catching)
return DeviceManager.getInstance();
// Experimental - seems reuse does not work
} catch (IllegalStateException e) {
// Exception caused by first call to the library
return DeviceManager.createInstance(false);
}
}, scheduler);
stage1.thenCompose(devManager -> {
// lambdas can't modify outside variables due to scoping, so instead we use an AtomicInteger.
AtomicInteger tryCount = new AtomicInteger();
// We need to set deviceManagerWrapperFuture here since we want to be able to cancel the underlying
// AsyncCompletableFuture instance
return this.deviceManagerWrapperFuture = callAsync(() -> {
int count = tryCount.incrementAndGet();
try {
logger.debug("Registering property handler attempt: {}", count);
devManager.registerPropertyHandler(changeHandler);
logger.debug("Successfully registered property handler");
return new DeviceManagerWrapper(devManager);
} catch (DBusException e) {
if (count < 3) {
throw new RetryException(5, TimeUnit.SECONDS);
} else {
throw e;
}
}
}, scheduler);
}).whenComplete((devManagerWrapper, th) -> {
if (th != null) {
logger.warn("Failed to initialize DeviceManager: {}", th.getMessage());
}
});
}
@Deactivate
public void dispose() {
var stage1 = this.deviceManagerFuture;
if (stage1 != null) {
if (!stage1.cancel(true)) {
// a failure to cancel means that the stage completed normally
stage1.thenAccept(DeviceManager::closeConnection);
}
}
this.deviceManagerFuture = null;
var stage2 = this.deviceManagerWrapperFuture;
if (stage2 != null) {
stage2.cancel(true);
}
this.deviceManagerWrapperFuture = null;
}
private static <T> CompletableFuture<T> callAsync(Callable<T> callable, ScheduledExecutorService scheduler) {
return new AsyncCompletableFuture<>(callable, scheduler);
}
// this is a utility class that allows use of Callable with CompletableFutures in a way such that the
// async future is cancellable thru this CompletableFuture instance.
private static class AsyncCompletableFuture<T> extends CompletableFuture<T> implements Runnable {
private final Callable<T> callable;
private final ScheduledExecutorService scheduler;
private final Object futureLock = new Object();
private Future<?> future;
public AsyncCompletableFuture(Callable<T> callable, ScheduledExecutorService scheduler) {
this.callable = callable;
this.scheduler = scheduler;
future = scheduler.submit(this);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
synchronized (futureLock) {
future.cancel(mayInterruptIfRunning);
}
return super.cancel(mayInterruptIfRunning);
}
@Override
public void run() {
try {
complete(callable.call());
} catch (RetryException e) {
synchronized (futureLock) {
if (!future.isCancelled()) {
future = scheduler.schedule(this, e.delay, e.unit);
}
}
} catch (Exception e) {
completeExceptionally(e);
}
}
}
// this is a special exception to indicate to a AsyncCompletableFuture that the task needs to be retried.
private static class RetryException extends Exception {
private static final long serialVersionUID = 8512275408512109328L;
private long delay;
private TimeUnit unit;
public RetryException(long delay, TimeUnit unit) {
this.delay = delay;
this.unit = unit;
}
}
}

View File

@ -0,0 +1,63 @@
/**
* 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.bluez.internal;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAddress;
import com.github.hypfvieh.bluetooth.DeviceManager;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter;
import com.github.hypfvieh.bluetooth.wrapper.BluetoothDevice;
/**
* This is a threadsafe wrapper for a {@link DeviceManager} that also only exposes the methods
* required to implement this binding.
*
* @author Connor Petty - Initial Contribution
*/
@NonNullByDefault
public class DeviceManagerWrapper {
private DeviceManager deviceManager;
public DeviceManagerWrapper(DeviceManager deviceManager) {
this.deviceManager = deviceManager;
}
public synchronized Collection<BluetoothAdapter> scanForBluetoothAdapters() {
return deviceManager.scanForBluetoothAdapters();
}
public synchronized @Nullable BluetoothAdapter getAdapter(BluetoothAddress address) {
// we don't use `deviceManager.getAdapter` here since it might perform a scan if the adapter is missing.
String addr = address.toString();
List<BluetoothAdapter> adapters = deviceManager.getAdapters();
if (adapters != null) {
for (BluetoothAdapter btAdapter : adapters) {
String btAddr = btAdapter.getAddress();
if (addr.equalsIgnoreCase(btAddr)) {
return btAdapter;
}
}
}
return null;
}
public synchronized List<BluetoothDevice> getDevices(BluetoothAdapter adapter) {
return deviceManager.getDevices(adapter.getAddress(), true);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This is triggered when a bluetooth adapter's 'Discovering' property changes
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class AdapterDiscoveringChangedEvent extends BlueZEvent {
private boolean discovering;
public AdapterDiscoveringChangedEvent(String dbusPath, boolean discovering) {
super(dbusPath);
this.discovering = discovering;
}
public boolean isDiscovering() {
return discovering;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onDiscoveringChanged(this);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This is triggered when a bluetooth adapter's 'Powered' property changes
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class AdapterPoweredChangedEvent extends BlueZEvent {
private boolean powered;
public AdapterPoweredChangedEvent(String dbusPath, boolean powered) {
super(dbusPath);
this.powered = powered;
}
public boolean isPowered() {
return powered;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onPoweredChange(this);
}
}

View File

@ -0,0 +1,84 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothAddress;
/**
* The {@link BlueZEvent} class represents an event from dbus due to
* changes in the properties of a bluetooth device.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public abstract class BlueZEvent {
private String dbusPath;
private @Nullable BluetoothAddress device;
private @Nullable String adapterName;
public BlueZEvent(String dbusPath) {
this.dbusPath = dbusPath;
// the rest of the code should be equivalent to parsing with the following regex:
// "/org/bluez/(?<adapterName>[^/]+)(/dev_(?<deviceMac>[^/]+).*)?"
if (!dbusPath.startsWith("/org/bluez/")) {
return;
}
int start = dbusPath.indexOf('/', 11);
if (start == -1) {
this.adapterName = dbusPath.substring(11);
return;
} else {
this.adapterName = dbusPath.substring(11, start);
}
start++;
int end = dbusPath.indexOf('/', start);
String mac;
if (end == -1) {
mac = dbusPath.substring(start);
} else {
mac = dbusPath.substring(start, end);
}
if (!mac.startsWith("dev_")) {
return;
}
mac = mac.substring(4); // trim off the "dev_" prefix
if (!mac.isEmpty()) {
this.device = new BluetoothAddress(mac.replace('_', ':').toUpperCase());
}
}
public String getDbusPath() {
return dbusPath;
}
public @Nullable BluetoothAddress getDevice() {
return device;
}
public @Nullable String getAdapterName() {
return adapterName;
}
public abstract void dispatch(BlueZEventListener listener);
@Override
public String toString() {
return getClass().getSimpleName() + ": " + dbusPath;
}
}

View File

@ -0,0 +1,63 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This is the listener interface for BlueZEvents.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public interface BlueZEventListener {
public void onDBusBlueZEvent(BlueZEvent event);
public default void onDiscoveringChanged(AdapterDiscoveringChangedEvent event) {
onDBusBlueZEvent(event);
}
public default void onPoweredChange(AdapterPoweredChangedEvent event) {
onDBusBlueZEvent(event);
}
public default void onRssiUpdate(RssiEvent event) {
onDBusBlueZEvent(event);
}
public default void onTxPowerUpdate(TXPowerEvent event) {
onDBusBlueZEvent(event);
}
public default void onCharacteristicNotify(CharacteristicUpdateEvent event) {
onDBusBlueZEvent(event);
}
public default void onManufacturerDataUpdate(ManufacturerDataEvent event) {
onDBusBlueZEvent(event);
}
public default void onConnectedStatusUpdate(ConnectedEvent event) {
onDBusBlueZEvent(event);
}
public default void onNameUpdate(NameEvent event) {
onDBusBlueZEvent(event);
}
public default void onServicesResolved(ServicesResolvedEvent event) {
onDBusBlueZEvent(event);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when a update notification is received for a characteristic.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class CharacteristicUpdateEvent extends BlueZEvent {
private byte[] data;
public CharacteristicUpdateEvent(String dbusPath, byte[] data) {
super(dbusPath);
this.data = data;
}
public byte[] getData() {
return data;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onCharacteristicNotify(this);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when a bluetooth device's 'Connected' property changes.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class ConnectedEvent extends BlueZEvent {
private boolean connected;
public ConnectedEvent(String dbusPath, boolean connected) {
super(dbusPath);
this.connected = connected;
}
public boolean isConnected() {
return connected;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onConnectedStatusUpdate(this);
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.bluez.internal.events;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when an update to a device's manufacturer data is received.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class ManufacturerDataEvent extends BlueZEvent {
private Map<Short, byte[]> data;
public ManufacturerDataEvent(String dbusPath, Map<Short, byte[]> data) {
super(dbusPath);
this.data = data;
}
public Map<Short, byte[]> getData() {
return data;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onManufacturerDataUpdate(this);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when a device's 'Name' bluez property changes
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class NameEvent extends BlueZEvent {
private String name;
public NameEvent(String dbusPath, String name) {
super(dbusPath);
this.name = name;
}
public String getName() {
return name;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onNameUpdate(this);
}
}

View File

@ -0,0 +1,41 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when bluetooth advertisement packet is picked up from a device.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class RssiEvent extends BlueZEvent {
private short rssi;
public RssiEvent(String dbusPath, short rssi) {
super(dbusPath);
this.rssi = rssi;
}
public short getRssi() {
return rssi;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onRssiUpdate(this);
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when a device's GATT services get resovled/unresolved.
* Services become resolved after connecting to a device and become unresolved
* either due to error or connection issues.
*
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class ServicesResolvedEvent extends BlueZEvent {
private boolean resolved;
public ServicesResolvedEvent(String dbusPath, boolean resolved) {
super(dbusPath);
this.resolved = resolved;
}
public boolean isResolved() {
return resolved;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onServicesResolved(this);
}
}

View File

@ -0,0 +1,42 @@
/**
* 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.bluez.internal.events;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This event is triggered when a device's 'TxPower' property is changed, typically due to receiving an advertisement
* packet from the device.
*
* @author Benjamin Lafois - Initial Contribution
*
*/
@NonNullByDefault
public class TXPowerEvent extends BlueZEvent {
private short txPower;
public TXPowerEvent(String dbusPath, short txpower) {
super(dbusPath);
this.txPower = txpower;
}
public short getTxPower() {
return this.txPower;
}
@Override
public void dispatch(BlueZEventListener listener) {
listener.onTxPowerUpdate(this);
}
}

View File

@ -1,22 +0,0 @@
/**
* 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
*/
@org.osgi.annotation.bundle.Header(name = org.osgi.framework.Constants.BUNDLE_NATIVECODE, value = "lib/armv6hf/libjavatinyb.so;lib/armv6hf/libtinyb.so;processor=arm;osname=linux, lib/x86-64/libjavatinyb.so;lib/x86-64/libtinyb.so;processor=amd64;osname=linux, *")
@org.osgi.annotation.bundle.Header(name = "Specification-Version", value = "0.5.0-28-gac6d308.0.5.0-28-gac6d308")
package org.openhab.binding.bluetooth.bluez;
/**
* Additional information for BlueZ package
*
* @author Jan N. Klug - Initial contribution
*
*/

View File

@ -0,0 +1,90 @@
/**
* 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.bluez.internal;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNull;
import org.junit.jupiter.api.Test;
import org.openhab.binding.bluetooth.BluetoothAddress;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEvent;
import org.openhab.binding.bluetooth.bluez.internal.events.BlueZEventListener;
/**
*
* @author Benjamin Lafois - Initial Contribution
* @author Connor Petty - Added additional test cases
*/
public class BlueZEventTest {
@Test
public void testDbusPathParser0() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dsqdsq/ds/dd");
assertEquals("hci0", event.getAdapterName());
assertNull(event.getDevice());
}
@Test
public void testDbusPathParser1() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dev_00_CC_3F_B2_7E_60");
assertEquals("hci0", event.getAdapterName());
assertEquals(new BluetoothAddress("00:CC:3F:B2:7E:60"), event.getDevice());
}
@Test
public void testDbusPathParser2() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dev_A4_34_D9_ED_D3_74/service0026/char0027");
assertEquals("hci0", event.getAdapterName());
assertEquals(new BluetoothAddress("A4:34:D9:ED:D3:74"), event.getDevice());
}
@Test
public void testDbusPathParser3() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dev_00_CC_3F_B2_7E_60/");
assertEquals("hci0", event.getAdapterName());
assertEquals(new BluetoothAddress("00:CC:3F:B2:7E:60"), event.getDevice());
}
@Test
public void testDbusPathParser4() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dev_");
assertEquals("hci0", event.getAdapterName());
assertNull(event.getDevice());
}
@Test
public void testDbusPathParser5() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0/dev_/");
assertEquals("hci0", event.getAdapterName());
assertNull(event.getDevice());
}
@Test
public void testDbusPathParser6() {
BlueZEvent event = new DummyBlueZEvent("/org/bluez/hci0");
assertEquals("hci0", event.getAdapterName());
assertNull(event.getDevice());
}
private static class DummyBlueZEvent extends BlueZEvent {
public DummyBlueZEvent(String dbusPath) {
super(dbusPath);
}
@Override
public void dispatch(@NonNull BlueZEventListener listener) {
listener.onDBusBlueZEvent(this);
}
}
}

View File

@ -2,6 +2,7 @@
<feature name="openhab-binding-bluetooth" description="Bluetooth Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle dependency="true">mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.airthings/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.am43/${project.version}</bundle>