diff --git a/bundles/org.openhab.binding.bluetooth.bluez/NOTICE b/bundles/org.openhab.binding.bluetooth.bluez/NOTICE index 9de213699..0f7ef793e 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/NOTICE +++ b/bundles/org.openhab.binding.bluetooth.bluez/NOTICE @@ -14,33 +14,33 @@ https://github.com/openhab/openhab-addons == Third-party Content -TinyB Version: 0.5.1 +BlueZ-DBus Version: 0.1.3 * License: MIT License -* Project: https://github.com/intel-iot-devkit/tinyb -* Source: https://github.com/intel-iot-devkit/tinyb/tree/v0.5.1 +* Project: https://github.com/hypfvieh/bluez-dbus +* Source: https://github.com/hypfvieh/bluez-dbus == Third-party license(s) === MIT License -The MIT License (MIT) -Copyright © 2015-2016 Intel Corporation +MIT License -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2017 David M. -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/bundles/org.openhab.binding.bluetooth.bluez/README.md b/bundles/org.openhab.binding.bluetooth.bluez/README.md index 5135e9075..4d7124cc2 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/README.md +++ b/bundles/org.openhab.binding.bluetooth.bluez/README.md @@ -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 diff --git a/bundles/org.openhab.binding.bluetooth.bluez/pom.xml b/bundles/org.openhab.binding.bluetooth.bluez/pom.xml index 152438e69..f25de595b 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/pom.xml +++ b/bundles/org.openhab.binding.bluetooth.bluez/pom.xml @@ -15,18 +15,21 @@ openHAB Add-ons :: Bundles :: BlueZ Bluetooth Adapter + org.openhab.addons.bundles org.openhab.binding.bluetooth ${project.version} provided + - org.openhab.osgiify - intel-iot-devkit.tinyb - 0.5.1 - compile + com.github.hypfvieh + bluez-dbus-osgi + 0.1.3 + provided + diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/feature/feature.xml b/bundles/org.openhab.binding.bluetooth.bluez/src/main/feature/feature.xml index 1119f1727..32d9d2766 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/feature/feature.xml @@ -4,7 +4,8 @@ openhab-runtime-base - openhab-transport-serial + + mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3 mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.bluez/${project.version} 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 index 0f099bde4..000000000 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZBluetoothDevice.java +++ /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 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 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/BlueZBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZBridgeHandler.java deleted file mode 100644 index 34a1d81eb..000000000 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZBridgeHandler.java +++ /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 { - - 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 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/handler/BlueZAdapterConfiguration.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConfiguration.java similarity index 71% rename from bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java rename to bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConfiguration.java index 0999624ee..1e3d8e6ad 100644 --- 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/internal/BlueZAdapterConfiguration.java @@ -10,16 +10,19 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.bluetooth.bluez.handler; +package org.openhab.binding.bluetooth.bluez.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration; /** - * Configuration properties class. + * Configuration properties for a bridge. * * @author Hilbrand Bouwkamp - Initial contribution */ +@NonNullByDefault public class BlueZAdapterConfiguration extends BaseBluetoothBridgeHandlerConfiguration { - public String address; + public @Nullable String address; } 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/internal/BlueZAdapterConstants.java similarity index 91% rename from bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/BlueZAdapterConstants.java rename to bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZAdapterConstants.java index 4edfada16..89a5f5569 100644 --- 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/internal/BlueZAdapterConstants.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.bluetooth.bluez; +package org.openhab.binding.bluetooth.bluez.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.bluetooth.BluetoothBindingConstants; @@ -30,4 +30,7 @@ public class BlueZAdapterConstants { // Properties public static final String PROPERTY_ADDRESS = "address"; + + private BlueZAdapterConstants() { + } } 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 index 000000000..d11b1bb64 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBluetoothDevice.java @@ -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 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 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 index 000000000..962b0ef62 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZBridgeHandler.java @@ -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 + 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 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/discovery/BlueZDiscoveryService.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZDiscoveryService.java similarity index 54% rename from bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/discovery/BlueZDiscoveryService.java rename to bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZDiscoveryService.java index 0ad4de71b..e6ddda926 100644 --- 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/BlueZDiscoveryService.java @@ -10,22 +10,26 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.bluetooth.bluez.internal.discovery; +package org.openhab.binding.bluetooth.bluez.internal; import java.util.Collections; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; -import org.openhab.binding.bluetooth.bluez.BlueZAdapterConstants; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import tinyb.BluetoothAdapter; -import tinyb.BluetoothManager; +import com.github.hypfvieh.bluetooth.wrapper.BluetoothAdapter; /** * This is a discovery service, which checks whether we are running on a Linux with a BlueZ stack. @@ -33,41 +37,59 @@ import tinyb.BluetoothManager; * * @author Kai Kreuzer - Initial Contribution and API * @author Hilbrand Bouwkamp - Moved background scan to actual background method + * @author Connor Petty - Replaced tinyB with bluezDbus * */ +@NonNullByDefault @Component(service = DiscoveryService.class, configurationPid = "discovery.bluetooth.bluez") public class BlueZDiscoveryService extends AbstractDiscoveryService { private final Logger logger = LoggerFactory.getLogger(BlueZDiscoveryService.class); - private BluetoothManager manager; + private final DeviceManagerFactory deviceManagerFactory; + private @Nullable Future backgroundScan; - public BlueZDiscoveryService() { + @Activate + public BlueZDiscoveryService(@Reference DeviceManagerFactory deviceManagerFactory) { super(Collections.singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ), 1, true); + this.deviceManagerFactory = deviceManagerFactory; + } + + private static void cancel(@Nullable Future future) { + if (future != null) { + future.cancel(false); + } } @Override protected void startBackgroundDiscovery() { - startScan(); + backgroundScan = scheduler.scheduleWithFixedDelay(() -> { + DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager(); + if (deviceManager == null) { + return; + } + startScan(); + }, 5, 10, TimeUnit.SECONDS); + } + + @Override + protected void stopBackgroundDiscovery() { + cancel(backgroundScan); + backgroundScan = null; } @Override protected void startScan() { - try { - manager = BluetoothManager.getBluetoothManager(); - manager.getAdapters().stream().map(this::createDiscoveryResult).forEach(this::thingDiscovered); - } catch (UnsatisfiedLinkError e) { - logger.debug("Not possible to initialize the BlueZ stack. ", e); + DeviceManagerWrapper deviceManager = deviceManagerFactory.getDeviceManager(); + if (deviceManager == null) { + logger.warn("The DeviceManager is not available"); return; - } catch (RuntimeException e) { - // we do not get anything more specific from TinyB here - if (e.getMessage() != null && e.getMessage().contains("AccessDenied")) { - logger.warn( - "Cannot access BlueZ stack due to permission problems. Make sure that your OS user is part of the 'bluetooth' group of BlueZ."); - } else { - logger.warn("Failed to scan for Bluetooth devices", e); - } } + // the first time the device manager is not null we can cancel background discovery + stopBackgroundDiscovery(); + deviceManager.scanForBluetoothAdapters().stream()// + .map(this::createDiscoveryResult)// + .forEach(this::thingDiscovered); } private DiscoveryResult createDiscoveryResult(BluetoothAdapter adapter) { @@ -78,6 +100,6 @@ public class BlueZDiscoveryService extends AbstractDiscoveryService { } private String getId(BluetoothAdapter adapter) { - return adapter.getInterfaceName().replaceAll("[^a-zA-Z0-9_]", ""); + return adapter.getDeviceName().replaceAll("[^a-zA-Z0-9_]", ""); } } diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZHandlerFactory.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZHandlerFactory.java index 59adb7130..0ddb3fa3d 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZHandlerFactory.java +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZHandlerFactory.java @@ -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 SUPPORTED_THING_TYPES_UIDS = Collections .singleton(BlueZAdapterConstants.THING_TYPE_BLUEZ); - private final Map> serviceRegs = new HashMap<>(); + private final Map> 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 index 000000000..db68b7610 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/BlueZPropertiesChangedHandler.java @@ -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 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 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 index 000000000..78ec6838b --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerFactory.java @@ -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 deviceManagerFuture; + private @Nullable CompletableFuture 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 CompletableFuture callAsync(Callable 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 extends CompletableFuture implements Runnable { + + private final Callable callable; + private final ScheduledExecutorService scheduler; + private final Object futureLock = new Object(); + private Future future; + + public AsyncCompletableFuture(Callable 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 index 000000000..484c1fe3d --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/DeviceManagerWrapper.java @@ -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 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 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 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/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 index 000000000..466ac750d --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterDiscoveringChangedEvent.java @@ -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 index 000000000..df68945e1 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/AdapterPoweredChangedEvent.java @@ -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 index 000000000..1b4928ebb --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEvent.java @@ -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/(?[^/]+)(/dev_(?[^/]+).*)?" + 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 index 000000000..24902862f --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/BlueZEventListener.java @@ -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 index 000000000..14526280c --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/CharacteristicUpdateEvent.java @@ -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 index 000000000..75a247d83 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ConnectedEvent.java @@ -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 index 000000000..255bba6f8 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ManufacturerDataEvent.java @@ -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 data; + + public ManufacturerDataEvent(String dbusPath, Map data) { + super(dbusPath); + this.data = data; + } + + public Map 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 index 000000000..08b06e3f0 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/NameEvent.java @@ -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 index 000000000..02bd858f4 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/RssiEvent.java @@ -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 index 000000000..006a51ae8 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/ServicesResolvedEvent.java @@ -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 index 000000000..c8b88d560 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/internal/events/TXPowerEvent.java @@ -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 index 40479c28e..000000000 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/package-info.java +++ /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 index b1b5da17f..000000000 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 index bf444a7f1..000000000 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 index 308abed35..000000000 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 index 430f3b6c4..000000000 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 index 000000000..4d2572bd1 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/test/java/org/openhab/binding/bluetooth/bluez/internal/BlueZEventTest.java @@ -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); + } + } +} diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml index 70dda1493..eeeabd0fd 100644 --- a/features/openhab-addons/src/main/resources/footer.xml +++ b/features/openhab-addons/src/main/resources/footer.xml @@ -2,6 +2,7 @@ openhab-runtime-base openhab-transport-serial + mvn:com.github.hypfvieh/bluez-dbus-osgi/0.1.3 mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.airthings/${project.version} mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.am43/${project.version}