mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[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:
parent
b3d0f027cf
commit
72bf43cfa0
@ -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.
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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_]", "");
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
*
|
||||
*/
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user