2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth;
15 import java.time.ZonedDateTime;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.concurrent.CopyOnWriteArraySet;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
26 import org.openhab.core.thing.Bridge;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.core.thing.Thing;
29 import org.openhab.core.thing.ThingUID;
30 import org.openhab.core.thing.binding.BaseBridgeHandler;
31 import org.openhab.core.types.Command;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * This is a abstract superclass for BluetoothAdapter implementations. This class takes care of inactive device cleanup
37 * as well as handling background and active discovery logic.
39 * Subclasses will primarily be responsible for device discovery
41 * @author Connor Petty - Initial contribution from refactored code
44 public abstract class AbstractBluetoothBridgeHandler<BD extends BaseBluetoothDevice> extends BaseBridgeHandler
45 implements BluetoothAdapter {
47 private final Logger logger = LoggerFactory.getLogger(AbstractBluetoothBridgeHandler.class);
49 // Set of discovery listeners
50 private final Set<BluetoothDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
52 // Map of Bluetooth devices known to this bridge.
53 // This contains the devices from the most recent scan
54 private final Map<BluetoothAddress, BD> devices = new ConcurrentHashMap<>();
56 // Actual discovery status.
57 protected volatile boolean activeScanEnabled = false;
59 private BaseBluetoothBridgeHandlerConfiguration config = new BaseBluetoothBridgeHandlerConfiguration();
61 private @Nullable ScheduledFuture<?> inactiveRemovalJob;
66 * @param bridge the bridge definition for this handler
68 public AbstractBluetoothBridgeHandler(Bridge bridge) {
73 public ThingUID getUID() {
74 return getThing().getUID();
78 public @Nullable String getLocation() {
79 return getThing().getLocation();
83 public @Nullable String getLabel() {
84 return getThing().getLabel();
88 public void initialize() {
89 config = getConfigAs(BaseBluetoothBridgeHandlerConfiguration.class);
91 int intervalSecs = config.inactiveDeviceCleanupInterval;
92 inactiveRemovalJob = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, intervalSecs, intervalSecs,
97 public void dispose() {
98 ScheduledFuture<?> inactiveRemovalJob = this.inactiveRemovalJob;
99 if (inactiveRemovalJob != null) {
100 inactiveRemovalJob.cancel(true);
102 this.inactiveRemovalJob = null;
104 synchronized (devices) {
105 for (BD device : devices.values()) {
106 removeDevice(device);
112 public void handleCommand(ChannelUID channelUID, Command command) {
115 private void removeInactiveDevices() {
116 // clean up orphaned entries
117 synchronized (devices) {
118 for (BD device : devices.values()) {
119 if (shouldRemove(device)) {
120 logger.debug("Removing device '{}' due to inactivity", device.getAddress());
121 removeDevice(device);
127 protected void removeDevice(BD device) {
129 synchronized (devices) {
130 devices.remove(device.getAddress());
132 discoveryListeners.forEach(listener -> listener.deviceRemoved(device));
135 private boolean shouldRemove(BD device) {
136 // we can't remove devices with listeners since that means they have a handler.
137 if (device.hasListeners()) {
140 // devices that are connected won't receive any scan notifications so we can't remove them for being idle
141 if (device.getConnectionState() == ConnectionState.CONNECTED) {
145 ZonedDateTime lastActiveTime = device.getLastSeenTime();
146 if (lastActiveTime == null) {
147 // we want any new device to at least live a certain amount of time so it has a chance to be discovered or
149 lastActiveTime = device.createTime;
151 // we remove devices we haven't seen in a while
152 return ZonedDateTime.now().minusSeconds(config.inactiveDeviceCleanupThreshold).isAfter(lastActiveTime);
156 public void addDiscoveryListener(BluetoothDiscoveryListener listener) {
157 discoveryListeners.add(listener);
161 public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) {
162 discoveryListeners.remove(listener);
166 public void scanStart() {
167 // Enable scanning even while discovery is disabled in config. This allows manual starting discovery.
168 activeScanEnabled = true;
169 refreshDiscoveredDevices();
172 protected void refreshDiscoveredDevices() {
173 logger.debug("Refreshing Bluetooth device list...");
174 synchronized (devices) {
175 devices.values().forEach(this::deviceDiscovered);
180 public void scanStop() {
181 // Set active discovery state back to the configured discovery state.
182 activeScanEnabled = false;
183 // We need to keep the adapter in discovery mode as we otherwise won't get any RSSI updates either
187 public BD getDevice(BluetoothAddress address) {
188 synchronized (devices) {
189 return devices.computeIfAbsent(address, this::createDevice);
193 protected abstract BD createDevice(BluetoothAddress address);
196 public boolean hasHandlerForDevice(BluetoothAddress address) {
197 String addrStr = address.toString();
199 * This type of search is inefficient and won't scale as the number of bluetooth Thing children increases on
200 * this bridge. But implementing a more efficient search would require a bit more overhead.
201 * Luckily though, it is reasonable to assume that the number of Thing children will remain small.
203 for (Thing childThing : getThing().getThings()) {
204 Object childAddr = childThing.getConfiguration().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS);
205 if (addrStr.equals(childAddr)) {
206 return childThing.getHandler() != null;
212 public void deviceDiscovered(BluetoothDevice device) {
213 if (hasHandlerForDevice(device.getAddress())) {
214 // no point in discovering a device that already has a handler
217 if (config.backgroundDiscovery || activeScanEnabled) {
218 if (deviceReachable(device)) {
219 discoveryListeners.forEach(listener -> listener.deviceDiscovered(device));
221 logger.trace("Not notifying listeners for device '{}', because it is not reachable.",
222 device.getAddress());
227 private boolean deviceReachable(BluetoothDevice device) {
228 Integer rssi = device.getRssi();
229 return rssi != null && rssi != 0;