]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth.bluez] Complete Bluez rewrite (#8819)
authorBenjamin Lafois <benjamin.lafois@gmail.com>
Sun, 25 Oct 2020 14:54:33 +0000 (15:54 +0100)
committerGitHub <noreply@github.com>
Sun, 25 Oct 2020 14:54:33 +0000 (15:54 +0100)
Also-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
Signed-off-by: Benjamin Lafois <benjamin.lafois@gmail.com>
36 files changed:
bundles/org.openhab.binding.bluetooth.bluez/NOTICE
bundles/org.openhab.binding.bluetooth.bluez/README.md
bundles/org.openhab.binding.bluetooth.bluez/pom.xml
bundles/org.openhab.binding.bluetooth.bluez/src/main/feature/feature.xml
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZAdapterConstants.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZBluetoothDevice.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZBridgeHandler.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZHandlerFactory.java
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/discovery/BlueZDiscoveryService.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterDiscoveringChangedEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterPoweredChangedEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/CharacteristicUpdateEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ConnectedEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ManufacturerDataEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/NameEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/RssiEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServicesResolvedEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/TXPowerEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/package-info.java [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libjavatinyb.so [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libtinyb.so [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libjavatinyb.so [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libtinyb.so [deleted file]
bundles/org.openhab.binding.bluetooth.bluez/src/test/java/org/openhab/binding/bluetooth/bluez/internal/BlueZEventTest.java [new file with mode: 0644]
features/openhab-addons/src/main/resources/footer.xml

index 9de21369948362b7c90172cdee4b61aab8f4f430..0f7ef793e8cf1832c2f176d4c56919d1fd6e3341 100644 (file)
@@ -14,33 +14,33 @@ https://github.com/openhab/openhab-addons
 
 == Third-party Content
 
-TinyB Version: 0.5.1 
+BlueZ-DBus Version: 0.1.3
 * License:  MIT License
-* Project: https://github.com/intel-iot-devkit/tinyb
-* Source: https://github.com/intel-iot-devkit/tinyb/tree/v0.5.1
+* Project: https://github.com/hypfvieh/bluez-dbus
+* Source: https://github.com/hypfvieh/bluez-dbus
 
 == Third-party license(s)
 
 === MIT License
 
-The MIT License (MIT)
-Copyright © 2015-2016 Intel Corporation
-
-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 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.
+MIT License
+
+Copyright (c) 2017 David M.
+
+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 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.
\ No newline at end of file
index 5135e9075508bb86985a04eedf7bdcdb847e642a..4d7124cc24c124506e842d0a37d4f69445da9560 100644 (file)
@@ -1,6 +1,6 @@
 # Bluetooth BlueZ Adapter
 
-This extension supports Bluetooth access via BlueZ on Linux (ARMv6hf).
+This extension supports Bluetooth access via BlueZ and DBus on Linux. This is architecture agnostic and uses Unix Sockets.
 
 # Setup
 
@@ -44,14 +44,15 @@ It defines the following bridge type:
 |----------------|---------------------------------------------------------------------------|
 | bluez          | A Bluetooth adapter that is supported by BlueZ                            |
 
-
 ## Discovery
 
 If BlueZ is enabled and can be accessed, all available adapters are automatically discovered.
 
+
 ## Bridge Configuration
 
 The bluez bridge requires the configuration parameter `address`, which corresponds to the Bluetooth address of the adapter (in format "XX:XX:XX:XX:XX:XX").
+
 Additionally, the parameter `backgroundDiscovery` can be set to true/false.When set to true, any Bluetooth device of which broadcasts are received is added to the Inbox.
 
 ## Example
index 152438e699f2201f2075c05a0f0c591d47072b84..f25de595b4a2a888c20db16b54ded84fb5d84ac3 100644 (file)
   <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>
index 1119f1727a4b1b6cf5c4aa5acae62aa50d2e27b6..32d9d2766b556663e643f68bc856221f4f68c54e 100644 (file)
@@ -4,7 +4,8 @@
 
        <feature name="openhab-binding-bluetooth-bluez" description="Bluetooth Binding Bluez" version="${project.version}">
                <feature>openhab-runtime-base</feature>
-               <feature>openhab-transport-serial</feature>
+
+               <bundle dependency="true">mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version}</bundle>
        </feature>
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZAdapterConstants.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZAdapterConstants.java
deleted file mode 100644 (file)
index 4edfada..0000000
+++ /dev/null
@@ -1,33 +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 org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.bluetooth.BluetoothBindingConstants;
-import org.openhab.core.thing.ThingTypeUID;
-
-/**
- * The {@link BlueZAdapterConstants} class defines common constants, which are
- * used across the whole binding.
- *
- * @author Kai Kreuzer - Initial contribution and API
- */
-@NonNullByDefault
-public class BlueZAdapterConstants {
-
-    // List of all Thing Type UIDs
-    public static final ThingTypeUID THING_TYPE_BLUEZ = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "bluez");
-
-    // Properties
-    public static final String PROPERTY_ADDRESS = "address";
-}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZBluetoothDevice.java
deleted file mode 100644 (file)
index 0f099bd..0000000
+++ /dev/null
@@ -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());
-            }
-        }
-    }
-}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java
deleted file mode 100644 (file)
index 0999624..0000000
+++ /dev/null
@@ -1,25 +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 org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration;
-
-/**
- * Configuration properties class.
- *
- * @author Hilbrand Bouwkamp - Initial contribution
- */
-public class BlueZAdapterConfiguration extends BaseBluetoothBridgeHandlerConfiguration {
-
-    public String address;
-}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZBridgeHandler.java
deleted file mode 100644 (file)
index 34a1d81..0000000
+++ /dev/null
@@ -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();
-    }
-}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConfiguration.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConfiguration.java
new file mode 100644 (file)
index 0000000..1e3d8e6
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration;
+
+/**
+ * Configuration properties for a bridge.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class BlueZAdapterConfiguration extends BaseBluetoothBridgeHandlerConfiguration {
+
+    public @Nullable String address;
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConstants.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConstants.java
new file mode 100644 (file)
index 0000000..89a5f55
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.bluetooth.BluetoothBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link BlueZAdapterConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+@NonNullByDefault
+public class BlueZAdapterConstants {
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_BLUEZ = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "bluez");
+
+    // Properties
+    public static final String PROPERTY_ADDRESS = "address";
+
+    private BlueZAdapterConstants() {
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java
new file mode 100644 (file)
index 0000000..d11b1bb
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBridgeHandler.java
new file mode 100644 (file)
index 0000000..962b0ef
--- /dev/null
@@ -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
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZDiscoveryService.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZDiscoveryService.java
new file mode 100644 (file)
index 0000000..e6ddda9
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * 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.Collections;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+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 com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter;
+
+/**
+ * This is a discovery service, which checks whether we are running on a Linux with a BlueZ stack.
+ * If this is the case, we create a bridge handler that provides Bluetooth access through BlueZ.
+ *
+ * @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 final DeviceManagerFactory deviceManagerFactory;
+    private @Nullable Future<?> backgroundScan;
+
+    @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() {
+        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() {
+        DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager();
+        if (deviceManager == null) {
+            logger.warn("The DeviceManager is not available");
+            return;
+        }
+        // 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) {
+        return DiscoveryResultBuilder.create(new ThingUID(BlueZAdapterConstants.THING_TYPE_BLUEZ, getId(adapter)))
+                .withLabel("Bluetooth Interface " + adapter.getName())
+                .withProperty(BlueZAdapterConstants.PROPERTY_ADDRESS, adapter.getAddress())
+                .withRepresentationProperty(BlueZAdapterConstants.PROPERTY_ADDRESS).build();
+    }
+
+    private String getId(BluetoothAdapter adapter) {
+        return adapter.getDeviceName().replaceAll("[^a-zA-Z0-9_]", "");
+    }
+}
index 59adb713017006b57e60a2cb69f6a3ff1d314f8c..0ddb3fa3d0df815f9a345cfc9f96e80469592ed1 100644 (file)
@@ -18,9 +18,9 @@ import java.util.Hashtable;
 import java.util.Map;
 import java.util.Set;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.bluetooth.BluetoothAdapter;
-import org.openhab.binding.bluetooth.bluez.BlueZAdapterConstants;
-import org.openhab.binding.bluetooth.bluez.handler.BlueZBridgeHandler;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
@@ -30,21 +30,32 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerFactory;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
 
 /**
  * The {@link BlueZHandlerFactory} is responsible for creating things and thing
  * handlers.
  *
  * @author Kai Kreuzer - Initial contribution and API
+ * @author Connor Petty - Added DeviceManagerFactory
  */
+@NonNullByDefault
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.bluetooth.bluez")
 public class BlueZHandlerFactory extends BaseThingHandlerFactory {
 
     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
             .singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ);
 
-    private final Map<ThingUID, ServiceRegistration<?>> serviceRegs = new HashMap<>();
+    private final Map<ThingUID, @Nullable ServiceRegistration<?>> serviceRegs = new HashMap<>();
+
+    private final DeviceManagerFactory deviceManagerFactory;
+
+    @Activate
+    public BlueZHandlerFactory(@Reference DeviceManagerFactory deviceManagerFactory) {
+        this.deviceManagerFactory = deviceManagerFactory;
+    }
 
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -52,11 +63,11 @@ public class BlueZHandlerFactory extends BaseThingHandlerFactory {
     }
 
     @Override
-    protected ThingHandler createHandler(Thing thing) {
+    protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (thingTypeUID.equals(BlueZAdapterConstants.THING_TYPE_BLUEZ)) {
-            BlueZBridgeHandler handler = new BlueZBridgeHandler((Bridge) thing);
+            BlueZBridgeHandler handler = new BlueZBridgeHandler((Bridge) thing, deviceManagerFactory);
             registerBluetoothAdapter(handler);
             return handler;
         } else {
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java
new file mode 100644 (file)
index 0000000..db68b76
--- /dev/null
@@ -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));
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java
new file mode 100644 (file)
index 0000000..78ec683
--- /dev/null
@@ -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;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java
new file mode 100644 (file)
index 0000000..484c1fe
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/discovery/BlueZDiscoveryService.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/discovery/BlueZDiscoveryService.java
deleted file mode 100644 (file)
index 0ad4de7..0000000
+++ /dev/null
@@ -1,83 +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.internal.discovery;
-
-import java.util.Collections;
-
-import org.openhab.binding.bluetooth.bluez.BlueZAdapterConstants;
-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.Component;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import tinyb.BluetoothAdapter;
-import tinyb.BluetoothManager;
-
-/**
- * This is a discovery service, which checks whether we are running on a Linux with a BlueZ stack.
- * If this is the case, we create a bridge handler that provides Bluetooth access through BlueZ.
- *
- * @author Kai Kreuzer - Initial Contribution and API
- * @author Hilbrand Bouwkamp - Moved background scan to actual background method
- *
- */
-@Component(service = DiscoveryService.class, configurationPid = "discovery.bluetooth.bluez")
-public class BlueZDiscoveryService extends AbstractDiscoveryService {
-
-    private final Logger logger = LoggerFactory.getLogger(BlueZDiscoveryService.class);
-
-    private BluetoothManager manager;
-
-    public BlueZDiscoveryService() {
-        super(Collections.singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ), 1, true);
-    }
-
-    @Override
-    protected void startBackgroundDiscovery() {
-        startScan();
-    }
-
-    @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);
-            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);
-            }
-        }
-    }
-
-    private DiscoveryResult createDiscoveryResult(BluetoothAdapter adapter) {
-        return DiscoveryResultBuilder.create(new ThingUID(BlueZAdapterConstants.THING_TYPE_BLUEZ, getId(adapter)))
-                .withLabel("Bluetooth Interface " + adapter.getName())
-                .withProperty(BlueZAdapterConstants.PROPERTY_ADDRESS, adapter.getAddress())
-                .withRepresentationProperty(BlueZAdapterConstants.PROPERTY_ADDRESS).build();
-    }
-
-    private String getId(BluetoothAdapter adapter) {
-        return adapter.getInterfaceName().replaceAll("[^a-zA-Z0-9_]", "");
-    }
-}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterDiscoveringChangedEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterDiscoveringChangedEvent.java
new file mode 100644 (file)
index 0000000..466ac75
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterPoweredChangedEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterPoweredChangedEvent.java
new file mode 100644 (file)
index 0000000..df68945
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEvent.java
new file mode 100644 (file)
index 0000000..1b4928e
--- /dev/null
@@ -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;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java
new file mode 100644 (file)
index 0000000..2490286
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/CharacteristicUpdateEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/CharacteristicUpdateEvent.java
new file mode 100644 (file)
index 0000000..1452628
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ConnectedEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ConnectedEvent.java
new file mode 100644 (file)
index 0000000..75a247d
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ManufacturerDataEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ManufacturerDataEvent.java
new file mode 100644 (file)
index 0000000..255bba6
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/NameEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/NameEvent.java
new file mode 100644 (file)
index 0000000..08b06e3
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/RssiEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/RssiEvent.java
new file mode 100644 (file)
index 0000000..02bd858
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServicesResolvedEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServicesResolvedEvent.java
new file mode 100644 (file)
index 0000000..006a51a
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/TXPowerEvent.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/TXPowerEvent.java
new file mode 100644 (file)
index 0000000..c8b88d5
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/package-info.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/package-info.java
deleted file mode 100644 (file)
index 40479c2..0000000
+++ /dev/null
@@ -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
- *
- */
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libjavatinyb.so b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libjavatinyb.so
deleted file mode 100644 (file)
index b1b5da1..0000000
Binary files a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libjavatinyb.so and /dev/null differ
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libtinyb.so b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libtinyb.so
deleted file mode 100644 (file)
index bf444a7..0000000
Binary files a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/armv6hf/libtinyb.so and /dev/null differ
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libjavatinyb.so b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libjavatinyb.so
deleted file mode 100644 (file)
index 308abed..0000000
Binary files a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libjavatinyb.so and /dev/null differ
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libtinyb.so b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libtinyb.so
deleted file mode 100644 (file)
index 430f3b6..0000000
Binary files a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/lib/x86-64/libtinyb.so and /dev/null differ
diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/test/java/org/openhab/binding/bluetooth/bluez/internal/BlueZEventTest.java b/bundles/org.openhab.binding.bluetooth.bluez/src/test/java/org/openhab/binding/bluetooth/bluez/internal/BlueZEventTest.java
new file mode 100644 (file)
index 0000000..4d2572b
--- /dev/null
@@ -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);
+        }
+    }
+}
index 70dda14930f19af77aa09be8bdad26101361f3aa..eeeabd0fd1dc92c351c5a66cdfc4b0f3351cd567 100644 (file)
@@ -2,6 +2,7 @@
        <feature name="openhab-binding-bluetooth" description="Bluetooth Binding" version="${project.version}">
                <feature>openhab-runtime-base</feature>
                <feature>openhab-transport-serial</feature>
+               <bundle dependency="true">mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.airthings/${project.version}</bundle>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.am43/${project.version}</bundle>