diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java index e90aa8044f465..a8889a249ff5c 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/BlueGigaBluetoothDevice.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.bluetooth.bluegiga; -import java.time.ZonedDateTime; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -81,8 +80,6 @@ private enum BlueGigaProcedure { // The connection handle if the device is connected private int connection = -1; - private ZonedDateTime lastSeenTime = ZonedDateTime.now(); - private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth"); private @Nullable ScheduledFuture connectTimer; @@ -524,6 +521,7 @@ private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) { /** * Clean up and release memory. */ + @Override public void dispose() { if (connectionState == ConnectionState.CONNECTED) { disconnect(); @@ -536,19 +534,6 @@ public void dispose() { connection = -1; } - /** - * Return last seen Time - * - * @return last seen Time - */ - public ZonedDateTime getLastSeenTime() { - return lastSeenTime; - } - - private void updateLastSeenTime() { - this.lastSeenTime = ZonedDateTime.now(); - } - private void cancelTimer(@Nullable ScheduledFuture task) { if (task != null) { task.cancel(true); diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java index d1e326949c908..d34446fe5cc51 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/handler/BlueGigaBridgeHandler.java @@ -17,13 +17,10 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.time.ZonedDateTime; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -34,25 +31,17 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.eclipse.smarthome.core.thing.Bridge; -import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; -import org.eclipse.smarthome.core.thing.ThingUID; -import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; -import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.io.transport.serial.PortInUseException; import org.eclipse.smarthome.io.transport.serial.SerialPort; import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; import org.eclipse.smarthome.io.transport.serial.SerialPortManager; import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; -import org.openhab.binding.bluetooth.BluetoothAdapter; +import org.openhab.binding.bluetooth.AbstractBluetoothBridgeHandler; import org.openhab.binding.bluetooth.BluetoothAddress; import org.openhab.binding.bluetooth.BluetoothBindingConstants; -import org.openhab.binding.bluetooth.BluetoothDevice; -import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; -import org.openhab.binding.bluetooth.BluetoothDeviceListener; -import org.openhab.binding.bluetooth.BluetoothDiscoveryListener; import org.openhab.binding.bluetooth.bluegiga.BlueGigaAdapterConstants; import org.openhab.binding.bluetooth.bluegiga.BlueGigaBluetoothDevice; import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaCommand; @@ -117,8 +106,8 @@ * @author Pauli Anttila - Many improvements */ @NonNullByDefault -public class BlueGigaBridgeHandler extends BaseBridgeHandler - implements BluetoothAdapter, BlueGigaEventListener, BlueGigaHandlerListener { +public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler + implements BlueGigaEventListener, BlueGigaHandlerListener { private final Logger logger = LoggerFactory.getLogger(BlueGigaBridgeHandler.class); @@ -159,20 +148,12 @@ public class BlueGigaBridgeHandler extends BaseBridgeHandler // Map of open connections private final Map connections = new ConcurrentHashMap<>(); - // Set of discovery listeners - protected final Set discoveryListeners = new CopyOnWriteArraySet<>(); - - // List of device listeners - protected final ConcurrentHashMap deviceListeners = new ConcurrentHashMap<>(); - private volatile boolean initComplete = false; private @Nullable ScheduledFuture initTask; private @Nullable ScheduledFuture removeInactiveDevicesTask; private @Nullable ScheduledFuture discoveryTask; - private volatile boolean activeScanEnabled = false; - private @Nullable Future passiveScanIdleTimer; public BlueGigaBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) { @@ -180,19 +161,9 @@ public BlueGigaBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) this.serialPortManager = serialPortManager; } - @Override - public ThingUID getUID() { - // being a BluetoothAdapter, we use the UID of our bridge - return getThing().getUID(); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // No commands supported for the bridge - } - @Override public void initialize() { + super.initialize(); Optional cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class)); if (cfg.isPresent()) { configuration = cfg.get(); @@ -204,7 +175,12 @@ public void initialize() { @Override public void dispose() { - stop(true); + stop(); + stopScheduledTasks(); + if (initTask != null) { + initTask.cancel(true); + } + super.dispose(); } private void start() { @@ -212,7 +188,7 @@ private void start() { if (!initComplete) { logger.debug("Initialize BlueGiga"); logger.debug("Using configuration: {}", configuration); - stop(false); + stop(); if (openSerialPort(configuration.port, 115200)) { serialHandler = Optional.of(new BlueGigaSerialHandler(inputStream.get(), outputStream.get())); transactionManager = Optional.of(new BlueGigaTransactionManager(serialHandler.get(), executor)); @@ -258,7 +234,7 @@ private void start() { } } - private void stop(boolean exit) { + private void stop() { if (transactionManager.isPresent()) { transactionManager.get().removeEventListener(this); transactionManager.get().close(); @@ -273,17 +249,6 @@ private void stop(boolean exit) { initComplete = false; connections.clear(); closeSerialPort(); - - if (exit) { - stopScheduledTasks(); - if (initTask != null) { - initTask.cancel(true); - } - devices.forEach((address, device) -> { - device.dispose(); - }); - devices.clear(); - } } private void schedulePassiveScan() { @@ -308,8 +273,6 @@ private void cancelScheduledPassiveScan() { private void startScheduledTasks() { schedulePassiveScan(); logger.debug("Start scheduled task to remove inactive devices"); - removeInactiveDevicesTask = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, 1, 1, - TimeUnit.MINUTES); discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS); } @@ -325,38 +288,6 @@ private void stopScheduledTasks() { } } - private void removeInactiveDevices() { - logger.debug("Check inactive devices, count {}", devices.size()); - devices.forEach((address, device) -> { - if (shouldRemove(device)) { - logger.debug("Removing device '{}' due to inactivity, last seen: {}", address, - device.getLastSeenTime()); - device.dispose(); - devices.remove(address); - } - }); - } - - private void refreshDiscoveredDevices() { - logger.debug("Refreshing Bluetooth device list..."); - devices.forEach((address, device) -> { - deviceDiscovered(device); - }); - } - - private boolean shouldRemove(BlueGigaBluetoothDevice device) { - // we can't remove devices with listeners since that means they have a handler. - if (device.hasListeners()) { - return false; - } - // devices that are connected won't receive any scan notifications so we can't remove them for being idle - if (device.getConnectionState() == ConnectionState.CONNECTED) { - return false; - } - - return device.getLastSeenTime().plusMinutes(5).isBefore(ZonedDateTime.now()); - } - private BlueGigaGetConnectionsResponse readMaxConnections() throws BlueGigaException { return sendCommandWithoutChecks(new BlueGigaGetConnectionsCommand(), BlueGigaGetConnectionsResponse.class); } @@ -448,24 +379,20 @@ private void closeSerialPort() { @Override public void scanStart() { + super.scanStart(); logger.debug("Start active scan"); - activeScanEnabled = true; // Stop the passive scan cancelScheduledPassiveScan(); bgEndProcedure(); // Start a active scan bgStartScanning(true, configuration.activeScanInterval, configuration.activeScanWindow); - - for (BluetoothDevice device : devices.values()) { - deviceDiscovered(device); - } } @Override public void scanStop() { + super.scanStop(); logger.debug("Stop active scan"); - activeScanEnabled = false; // Stop the active scan bgEndProcedure(); @@ -484,21 +411,9 @@ public BluetoothAddress getAddress() { } } - @SuppressWarnings({ "null", "unused" }) @Override - public BluetoothDevice getDevice(BluetoothAddress address) { - BlueGigaBluetoothDevice device = devices.get(address); - if (device == null) { - // This method always needs to return a device, even if we don't currently know about it. - device = new BlueGigaBluetoothDevice(this, address, BluetoothAddressType.UNKNOWN); - devices.put(address, device); - } - return device; - } - - @Override - public boolean hasDevice(BluetoothAddress address) { - return devices.containsKey(address); + protected BlueGigaBluetoothDevice createDevice(BluetoothAddress address) { + return new BlueGigaBluetoothDevice(this, address, BluetoothAddressType.UNKNOWN); } /** @@ -566,17 +481,6 @@ public boolean bgDisconnect(int connectionHandle) { } } - /** - * Device discovered. This simply passes the discover information to the discovery service for processing. - */ - public void deviceDiscovered(BluetoothDevice device) { - if (configuration.discovery || activeScanEnabled) { - for (BluetoothDiscoveryListener listener : discoveryListeners) { - listener.deviceDiscovered(device); - } - } - } - /** * Start a read of all primary services using {@link BlueGigaReadByGroupTypeCommand} * @@ -795,16 +699,6 @@ public void removeEventListener(BlueGigaEventListener listener) { }); } - @Override - public void addDiscoveryListener(BluetoothDiscoveryListener listener) { - discoveryListeners.add(listener); - } - - @Override - public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) { - discoveryListeners.remove(listener); - } - @Override public void bluegigaEventReceived(@Nullable BlueGigaResponse event) { if (event instanceof BlueGigaScanResponseEvent) { diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaConfiguration.java b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaConfiguration.java index 418820a7699dd..3fa5a1f2c72c2 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaConfiguration.java +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/java/org/openhab/binding/bluetooth/bluegiga/internal/BlueGigaConfiguration.java @@ -13,6 +13,7 @@ package org.openhab.binding.bluetooth.bluegiga.internal; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration; /** * Configuration class for {@link BlueGigaConfiguration} device. @@ -20,8 +21,7 @@ * @author Pauli Anttila - Initial contribution */ @NonNullByDefault -public class BlueGigaConfiguration { - public boolean discovery; +public class BlueGigaConfiguration extends BaseBluetoothBridgeHandlerConfiguration { public String port = ""; public int passiveScanIdleTime; public int passiveScanInterval; @@ -39,7 +39,7 @@ public String toString() { "[discovery=%b, port=%s, passiveScanIdleTime=%d, passiveScanInterval=%d, passiveScanWindow=%d" + ", activeScanInterval=%d, activeScanWindow=%d, connIntervalMin=%d, connIntervalMax=%d" + ", connLatency=%d, connTimeout=%d]", - discovery, port, passiveScanIdleTime, passiveScanInterval, passiveScanWindow, activeScanInterval, - activeScanWindow, connIntervalMin, connIntervalMax, connLatency, connTimeout); + backgroundDiscovery, port, passiveScanIdleTime, passiveScanInterval, passiveScanWindow, + activeScanInterval, activeScanWindow, connIntervalMin, connIntervalMax, connLatency, connTimeout); } } diff --git a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/resources/ESH-INF/thing/bluegiga.xml b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/resources/ESH-INF/thing/bluegiga.xml index f208b32e4186a..ddef05a56a1f3 100644 --- a/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/resources/ESH-INF/thing/bluegiga.xml +++ b/bundles/org.openhab.binding.bluetooth.bluegiga/src/main/resources/ESH-INF/thing/bluegiga.xml @@ -14,12 +14,24 @@ serial-port Serial Port - - - Whether this adapter actively participates in Bluetooth device discovery + + + Whether this adapter performs background discovery of Bluetooth devices true false + + + How often device cleanup is performed + true + 60 + + + + Timespan a device can remain radio silent before it is eligible for cleanup + true + 300 + Passive scan idle time defines the time how long to wait in milliseconds before start passive scan. 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 index 9e4a0a513d435..dd63b4cd21d04 100644 --- 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 @@ -51,8 +51,6 @@ public class BlueZBluetoothDevice extends BluetoothDevice { private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth"); - private long lastSeenTime; - /** * Constructor * @@ -376,17 +374,10 @@ private BluetoothGattDescriptor getTinybDescriptorByUUID(String uuid) { return null; } - private void updateLastSeenTime() { - this.lastSeenTime = System.currentTimeMillis(); - } - - public long getTimeSinceSeen(TimeUnit unit) { - return unit.convert(System.currentTimeMillis() - this.lastSeenTime, TimeUnit.MILLISECONDS); - } - /** * Clean up and release memory. */ + @Override public void dispose() { if (device == null) { return; diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java b/bundles/org.openhab.binding.bluetooth.bluez/src/main/java/org/openhab/binding/bluetooth/bluez/handler/BlueZAdapterConfiguration.java index 43e874c230a18..b85ce17496e9e 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/handler/BlueZAdapterConfiguration.java @@ -12,14 +12,15 @@ */ package org.openhab.binding.bluetooth.bluez.handler; +import org.openhab.binding.bluetooth.BaseBluetoothBridgeHandlerConfiguration; + /** * Configuration properties class. * * @author Hilbrand Bouwkamp - Initial contribution */ -public class BlueZAdapterConfiguration { +public class BlueZAdapterConfiguration extends BaseBluetoothBridgeHandlerConfiguration { public String address; - public Boolean discovery; } 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 index 60d32287446d0..ef0b12d886324 100644 --- 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 @@ -13,28 +13,15 @@ package org.openhab.binding.bluetooth.bluez.handler; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.eclipse.jdt.annotation.DefaultLocation; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.core.thing.Bridge; -import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingStatusDetail; -import org.eclipse.smarthome.core.thing.ThingUID; -import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; -import org.eclipse.smarthome.core.types.Command; -import org.openhab.binding.bluetooth.BluetoothAdapter; +import org.openhab.binding.bluetooth.AbstractBluetoothBridgeHandler; import org.openhab.binding.bluetooth.BluetoothAddress; -import org.openhab.binding.bluetooth.BluetoothDevice; -import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; -import org.openhab.binding.bluetooth.BluetoothDiscoveryListener; import org.openhab.binding.bluetooth.bluez.BlueZBluetoothDevice; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,30 +39,15 @@ * @author Connor Petty - Simplified device scan logic */ @NonNullByDefault -public class BlueZBridgeHandler extends BaseBridgeHandler implements BluetoothAdapter { +public class BlueZBridgeHandler extends AbstractBluetoothBridgeHandler { private final Logger logger = LoggerFactory.getLogger(BlueZBridgeHandler.class); private @NonNullByDefault({}) tinyb.BluetoothAdapter adapter; - private final Map tinybDeviceCache = new ConcurrentHashMap<>(); - // Our BT address private @NonNullByDefault({}) BluetoothAddress adapterAddress; - // Internal flag for the discovery configuration - private boolean discoveryConfigActive = true; - // Actual discovery status. - private boolean discoveryActive = true; - - // Map of Bluetooth devices known to this bridge. - // This contains the devices from the most recent scan - private final @NonNullByDefault({ - DefaultLocation.RETURN_TYPE }) Map devices = new ConcurrentHashMap<>(); - - // Set of discovery listeners - protected final Set discoveryListeners = new CopyOnWriteArraySet<>(); - private @NonNullByDefault({}) ScheduledFuture discoveryJob; /** @@ -89,6 +61,7 @@ public BlueZBridgeHandler(Bridge bridge) { @Override public void initialize() { + super.initialize(); BluetoothManager manager; try { manager = BluetoothManager.getBluetoothManager(); @@ -115,11 +88,6 @@ public void initialize() { return; } - discoveryActive = discoveryConfigActive = Boolean.TRUE.equals(configuration.discovery); - if (discoveryConfigActive) { - logger.debug("Deactivated discovery participation."); - } - logger.debug("Creating BlueZ adapter with address '{}'", adapterAddress); for (tinyb.BluetoothAdapter adapter : manager.getAdapters()) { @@ -136,25 +104,6 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No adapter for this address found."); } - @Override - public ThingUID getUID() { - return getThing().getUID(); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - } - - @Override - public void addDiscoveryListener(BluetoothDiscoveryListener listener) { - discoveryListeners.add(listener); - } - - @Override - public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) { - discoveryListeners.remove(listener); - } - private void startDiscovery() { // we need to make sure the adapter is powered first if (!adapter.getPowered()) { @@ -167,36 +116,23 @@ private void startDiscovery() { } private void refreshDevices() { - try { + 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); - notifyDiscoveryListeners(device); - } - // clean up orphaned entries - synchronized (devices) { - for (BlueZBluetoothDevice device : devices.values()) { - if (shouldRemove(device)) { - logger.debug("Removing device '{}' due to inactivity", device.getAddress()); - device.dispose(); - devices.remove(device.getAddress()); - discoveryListeners.forEach(listener -> listener.deviceRemoved(device)); - } - } + deviceDiscovered(device); } // For whatever reason, bluez will sometimes turn off scanning. So we just make sure it keeps running. startDiscovery(); - // everything went fine, so lets switch to online - updateStatus(ThingStatus.ONLINE); } 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 - return; + break refreshTry; } int idx = message.lastIndexOf(':'); if (idx != -1) { @@ -204,33 +140,9 @@ private void refreshDevices() { } } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); + return; } - } - - private boolean shouldRemove(BlueZBluetoothDevice device) { - // we can't remove devices with listeners since that means they have a handler. - if (device.hasListeners()) { - return false; - } - // devices that are connected won't receive any scan notifications so we can't remove them for being idle - if (device.getConnectionState() == ConnectionState.CONNECTED) { - return false; - } - // we remove devices we haven't seen in a while - return device.getTimeSinceSeen(TimeUnit.MINUTES) > 5; - } - - @Override - public void scanStart() { - // Enable scanning even while discovery is disabled in config. This allows manual starting discovery. - discoveryActive = true; - } - - @Override - public void scanStop() { - // Set active discovery state back to the configured discovery state. - discoveryActive = discoveryConfigActive; - // We need to keep the adapter in discovery mode as we otherwise won't get any RSSI updates either + updateStatus(ThingStatus.ONLINE); } @Override @@ -239,21 +151,8 @@ public BluetoothAddress getAddress() { } @Override - public BlueZBluetoothDevice getDevice(BluetoothAddress bluetoothAddress) { - synchronized (devices) { - BlueZBluetoothDevice device = devices.get(bluetoothAddress); - if (device == null) { - device = new BlueZBluetoothDevice(this, bluetoothAddress); - device.initialize(); - devices.put(bluetoothAddress, device); - } - return device; - } - } - - @Override - public boolean hasDevice(BluetoothAddress address) { - return devices.containsKey(address); + protected BlueZBluetoothDevice createDevice(BluetoothAddress address) { + return new BlueZBluetoothDevice(this, address); } @Override @@ -265,30 +164,7 @@ public void dispose() { if (adapter != null && adapter.getDiscovering()) { adapter.stopDiscovery(); } - synchronized (devices) { - for (BlueZBluetoothDevice device : devices.values()) { - device.dispose(); - } - devices.clear(); - } - } - - private void notifyDiscoveryListeners(BluetoothDevice device) { - if (discoveryActive) { - if (deviceReachable(device)) { - for (BluetoothDiscoveryListener listener : discoveryListeners) { - listener.deviceDiscovered(device); - } - } else { - logger.trace("Not notifying listeners for device '{}', because it is not reachable.", - device.getAddress()); - } - } - } - - private boolean deviceReachable(BluetoothDevice device) { - Integer rssi = device.getRssi(); - return rssi != null && rssi != 0; + super.dispose(); } } diff --git a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/ESH-INF/thing/bluez.xml b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/ESH-INF/thing/bluez.xml index 2236bbe2b12ae..ec8fdc6a9e502 100644 --- a/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/ESH-INF/thing/bluez.xml +++ b/bundles/org.openhab.binding.bluetooth.bluez/src/main/resources/ESH-INF/thing/bluez.xml @@ -14,12 +14,24 @@ The Bluetooth address of the adapter in format XX:XX:XX:XX:XX:XX - - - Whether Bluetooth device discovery is always enabled for this adapter + + + Whether this adapter performs background discovery of Bluetooth devices true false + + + How often device cleanup is performed + true + 60 + + + + Timespan a device can remain radio silent before it is eligible for cleanup + true + 300 + diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/AbstractBluetoothBridgeHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/AbstractBluetoothBridgeHandler.java new file mode 100644 index 0000000000000..5ba2eb780ce32 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/AbstractBluetoothBridgeHandler.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.thing.Bridge; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is a abstract superclass for BluetoothAdapter implementations. This class takes care of inactive device cleanup + * as well as handling background and active discovery logic. + * + * Subclasses will primarily be responsible for device discovery + * + * @author Connor Petty - Initial contribution from refactored code + */ +@NonNullByDefault +public abstract class AbstractBluetoothBridgeHandler extends BaseBridgeHandler + implements BluetoothAdapter { + + private final Logger logger = LoggerFactory.getLogger(AbstractBluetoothBridgeHandler.class); + + // Set of discovery listeners + private final Set discoveryListeners = new CopyOnWriteArraySet<>(); + + // Map of Bluetooth devices known to this bridge. + // This contains the devices from the most recent scan + private final Map devices = new HashMap<>(); + + // Actual discovery status. + protected volatile boolean activeScanEnabled = false; + + private BaseBluetoothBridgeHandlerConfiguration config = new BaseBluetoothBridgeHandlerConfiguration(); + + private @Nullable ScheduledFuture inactiveRemovalJob; + + /** + * Constructor + * + * @param bridge the bridge definition for this handler + */ + public AbstractBluetoothBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public ThingUID getUID() { + return getThing().getUID(); + } + + @Override + public void initialize() { + config = getConfigAs(BaseBluetoothBridgeHandlerConfiguration.class); + + int intervalSecs = config.inactiveDeviceCleanupInterval; + inactiveRemovalJob = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, intervalSecs, intervalSecs, + TimeUnit.SECONDS); + } + + @Override + public void dispose() { + ScheduledFuture inactiveRemovalJob = this.inactiveRemovalJob; + if (inactiveRemovalJob != null) { + inactiveRemovalJob.cancel(true); + } + this.inactiveRemovalJob = null; + + synchronized (devices) { + for (BD device : devices.values()) { + removeDevice(device); + } + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + private void removeInactiveDevices() { + // clean up orphaned entries + synchronized (devices) { + for (BD device : devices.values()) { + if (shouldRemove(device)) { + logger.debug("Removing device '{}' due to inactivity", device.getAddress()); + removeDevice(device); + } + } + } + } + + protected void removeDevice(BluetoothDevice device) { + device.dispose(); + synchronized (devices) { + devices.remove(device.getAddress()); + } + discoveryListeners.forEach(listener -> listener.deviceRemoved(device)); + } + + private boolean shouldRemove(BluetoothDevice device) { + // we can't remove devices with listeners since that means they have a handler. + if (device.hasListeners()) { + return false; + } + // devices that are connected won't receive any scan notifications so we can't remove them for being idle + if (device.getConnectionState() == ConnectionState.CONNECTED) { + return false; + } + + // we remove devices we haven't seen in a while + return ZonedDateTime.now().minusSeconds(config.inactiveDeviceCleanupThreshold) + .isAfter(device.getLastSeenTime()); + } + + @Override + public void addDiscoveryListener(BluetoothDiscoveryListener listener) { + discoveryListeners.add(listener); + } + + @Override + public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) { + discoveryListeners.remove(listener); + } + + @Override + public void scanStart() { + // Enable scanning even while discovery is disabled in config. This allows manual starting discovery. + activeScanEnabled = true; + refreshDiscoveredDevices(); + } + + protected void refreshDiscoveredDevices() { + logger.debug("Refreshing Bluetooth device list..."); + synchronized (devices) { + devices.values().forEach(this::deviceDiscovered); + } + } + + @Override + public void scanStop() { + // Set active discovery state back to the configured discovery state. + activeScanEnabled = false; + // We need to keep the adapter in discovery mode as we otherwise won't get any RSSI updates either + } + + @Override + public BD getDevice(BluetoothAddress address) { + synchronized (devices) { + if (devices.containsKey(address)) { + return devices.get(address); + } + BD device = createDevice(address); + device.updateLastSeenTime(); + devices.put(address, device); + return device; + } + } + + protected abstract BD createDevice(BluetoothAddress address); + + @Override + public boolean hasHandlerForDevice(BluetoothAddress address) { + String addrStr = address.toString(); + /* + * This type of search is inefficient and won't scale as the number of bluetooth Thing children increases on + * this bridge. But implementing a more efficient search would require a bit more overhead. + * Luckily though, it is reasonable to assume that the number of Thing children will remain small. + */ + for (Thing childThing : getThing().getThings()) { + Object childAddr = childThing.getConfiguration().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS); + if (addrStr.equals(childAddr)) { + return childThing.getHandler() != null; + } + } + return false; + } + + public void deviceDiscovered(BluetoothDevice device) { + if (hasHandlerForDevice(device.getAddress())) { + // no point in discovering a device that already has a handler + return; + } + if (config.backgroundDiscovery || activeScanEnabled) { + if (deviceReachable(device)) { + discoveryListeners.forEach(listener -> listener.deviceDiscovered(device)); + } else { + logger.trace("Not notifying listeners for device '{}', because it is not reachable.", + device.getAddress()); + } + } + } + + private boolean deviceReachable(BluetoothDevice device) { + Integer rssi = device.getRssi(); + return rssi != null && rssi != 0; + } + +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothBridgeHandlerConfiguration.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothBridgeHandlerConfiguration.java new file mode 100644 index 0000000000000..904db763c74f0 --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BaseBluetoothBridgeHandlerConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.bluetooth; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This is the base configuration that all bluetooth bridge implementations will use. + * Bridges may choose to use a subclass of this class as their configuration in order to + * support more options. + * + * @author Connor Petty - Initial contribution from refactored code + */ +@NonNullByDefault +public class BaseBluetoothBridgeHandlerConfiguration { + public boolean backgroundDiscovery = false; + public int inactiveDeviceCleanupInterval = 60; + public int inactiveDeviceCleanupThreshold = 300; +} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java index f08714d33f516..3149716c0a52c 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BeaconBluetoothHandler.java @@ -26,7 +26,6 @@ import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.eclipse.smarthome.core.types.UnDefType; -import org.openhab.binding.bluetooth.discovery.internal.BluetoothAddressLocker; import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification; import org.openhab.binding.bluetooth.notification.BluetoothScanNotification; @@ -81,11 +80,9 @@ public void initialize() { try { deviceLock.lock(); - BluetoothAddressLocker.lock(address); device = adapter.getDevice(address); device.addListener(this); } finally { - BluetoothAddressLocker.unlock(address); deviceLock.unlock(); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothAdapter.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothAdapter.java index fabdb2286555b..da3ff8fe92b6f 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothAdapter.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothAdapter.java @@ -81,6 +81,6 @@ public interface BluetoothAdapter extends Identifiable { * @param address the {@link BluetoothAddress} to check for * @return true if this adapter has a {@link BluetoothDevice} with that address */ - boolean hasDevice(BluetoothAddress address); + boolean hasHandlerForDevice(BluetoothAddress address); } diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java index cc8c35eb74bc7..dc2a9e706f52b 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/BluetoothDevice.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.bluetooth; +import java.time.ZonedDateTime; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -126,6 +127,11 @@ protected enum BluetoothEventType { */ protected @Nullable Integer txPower = null; + /** + * Last time when activity occurred on this device. + */ + protected ZonedDateTime lastSeenTime; + /** * The event listeners will be notified of device updates */ @@ -140,10 +146,29 @@ protected enum BluetoothEventType { public BluetoothDevice(BluetoothAdapter adapter, BluetoothAddress address) { this.address = address; this.adapter = adapter; + this.lastSeenTime = ZonedDateTime.now(); + } + + /** + * Returns the last time this device was active + * + * @return The last time this device was active + */ + public ZonedDateTime getLastSeenTime() { + return lastSeenTime; } /** - * Returns the the name of the Bluetooth device. + * Updates the last activity timestamp for this device. + * Should be called whenever activity occurs on this device. + * + */ + public void updateLastSeenTime() { + lastSeenTime = ZonedDateTime.now(); + } + + /** + * Returns the name of the Bluetooth device. * * @return The devices name */ @@ -594,6 +619,14 @@ public boolean hasListeners() { return !eventListeners.isEmpty(); } + /** + * Releases resources that this device is using. + * + */ + protected void dispose() { + + } + /** * Notify the listeners of an event * diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothAddressLocker.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothAddressLocker.java deleted file mode 100644 index 0800722572e54..0000000000000 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothAddressLocker.java +++ /dev/null @@ -1,55 +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.discovery.internal; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import org.openhab.binding.bluetooth.BluetoothAddress; - -/** - * The {@link BluetoothAddressLocker} handles global locking of BluetoothAddress. - * This is used to make sure that devices with handlers are not connected to during discovery. - * - * @author Connor Petty - Initial Contribution - */ -public class BluetoothAddressLocker { - - private static Map locks = new ConcurrentHashMap<>(); - - public static void lock(BluetoothAddress address) { - locks.compute(address, (addr, oldRef) -> { - LockReference ref = oldRef; - if (ref == null) { - ref = new LockReference(); - } - ref.count++; - return ref; - }).lock.lock(); - } - - public static void unlock(BluetoothAddress address) { - locks.computeIfPresent(address, (addr, ref) -> { - ref.count--; - ref.lock.unlock(); - return ref.count <= 0 ? null : ref; - }); - } - - private static class LockReference { - private int count = 0; - private Lock lock = new ReentrantLock(); - } -} diff --git a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java index dad01c4c91e65..99d53c327e9c8 100644 --- a/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java +++ b/bundles/org.openhab.binding.bluetooth/src/main/java/org/openhab/binding/bluetooth/discovery/internal/BluetoothDiscoveryProcess.java @@ -107,24 +107,19 @@ public DiscoveryResult get() { DiscoveryResult result = null; if (!connectionParticipants.isEmpty()) { BluetoothAddress address = device.getAddress(); - try { - BluetoothAddressLocker.lock(address); - if (isAddressAvailable(address)) { - result = findConnectionResult(connectionParticipants); - // make sure to disconnect before letting go of the device - if (device.getConnectionState() == ConnectionState.CONNECTED) { - try { - if (!device.disconnect()) { - logger.debug("Failed to disconnect from device {}", address); - } - } catch (RuntimeException ex) { - logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", - address, device.getAdapter().getAddress(), ex); + if (isAddressAvailable(address)) { + result = findConnectionResult(connectionParticipants); + // make sure to disconnect before letting go of the device + if (device.getConnectionState() == ConnectionState.CONNECTED) { + try { + if (!device.disconnect()) { + logger.debug("Failed to disconnect from device {}", address); } + } catch (RuntimeException ex) { + logger.warn("Error occurred during bluetooth discovery for device {} on adapter {}", address, + device.getAdapter().getAddress(), ex); } } - } finally { - BluetoothAddressLocker.unlock(address); } } if (result == null) { @@ -135,11 +130,7 @@ public DiscoveryResult get() { private boolean isAddressAvailable(BluetoothAddress address) { // if a device with this address has a handler on any of the adapters, we abandon discovery - return adapters.stream().filter(adapter -> adapter.hasDevice(address)) - // get adapter's corresponding device - .map(adapter -> adapter.getDevice(address)) - // make sure nothing is listening to any of them - .noneMatch(d -> d.hasListeners()); + return adapters.stream().noneMatch(adapter -> adapter.hasHandlerForDevice(address)); } private DiscoveryResult createDefaultResult(BluetoothDevice device) { diff --git a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothAdapter.java b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothAdapter.java index bd831142415a5..d5ace31518b34 100644 --- a/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothAdapter.java +++ b/bundles/org.openhab.binding.bluetooth/src/test/java/org/openhab/binding/bluetooth/MockBluetoothAdapter.java @@ -63,8 +63,8 @@ public MockBluetoothDevice getDevice(BluetoothAddress address) { } @Override - public boolean hasDevice(BluetoothAddress address) { - return devices.containsKey(address); + public boolean hasHandlerForDevice(BluetoothAddress address) { + return false; } }