2 * Copyright (c) 2010-2023 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;
17 import java.util.Objects;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.CopyOnWriteArraySet;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
27 import org.openhab.core.thing.Bridge;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingUID;
31 import org.openhab.core.thing.binding.BaseBridgeHandler;
32 import org.openhab.core.types.Command;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
37 * This is an abstract superclass for BluetoothAdapter implementations. This class takes care of inactive device cleanup
38 * as well as handling background and active discovery logic.
40 * Subclasses will primarily be responsible for device discovery
42 * @author Connor Petty - Initial contribution from refactored code
45 public abstract class AbstractBluetoothBridgeHandler<BD extends BaseBluetoothDevice> extends BaseBridgeHandler
46 implements BluetoothAdapter {
48 private final Logger logger = LoggerFactory.getLogger(AbstractBluetoothBridgeHandler.class);
50 // Set of discovery listeners
51 private final Set<BluetoothDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
53 // Map of Bluetooth devices known to this bridge.
54 // This contains the devices from the most recent scan
55 private final Map<BluetoothAddress, BD> devices = new ConcurrentHashMap<>();
57 // Actual discovery status.
58 protected volatile boolean activeScanEnabled = false;
60 private BaseBluetoothBridgeHandlerConfiguration config = new BaseBluetoothBridgeHandlerConfiguration();
62 private @Nullable ScheduledFuture<?> inactiveRemovalJob;
67 * @param bridge the bridge definition for this handler
69 public AbstractBluetoothBridgeHandler(Bridge bridge) {
74 public ThingUID getUID() {
75 return getThing().getUID();
79 public @Nullable String getLocation() {
80 return getThing().getLocation();
84 public @Nullable String getLabel() {
85 return getThing().getLabel();
89 public void initialize() {
90 config = getConfigAs(BaseBluetoothBridgeHandlerConfiguration.class);
92 int intervalSecs = config.inactiveDeviceCleanupInterval;
93 inactiveRemovalJob = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, intervalSecs, intervalSecs,
98 public void dispose() {
99 ScheduledFuture<?> inactiveRemovalJob = this.inactiveRemovalJob;
100 if (inactiveRemovalJob != null) {
101 inactiveRemovalJob.cancel(true);
103 this.inactiveRemovalJob = null;
105 synchronized (devices) {
106 for (BD device : devices.values()) {
107 removeDevice(device);
113 public void handleCommand(ChannelUID channelUID, Command command) {
116 private void removeInactiveDevices() {
117 // clean up orphaned entries
118 synchronized (devices) {
119 for (BD device : devices.values()) {
120 if (shouldRemove(device)) {
121 logger.debug("Removing device '{}' due to inactivity", device.getAddress());
122 removeDevice(device);
128 protected void removeDevice(BD device) {
130 synchronized (devices) {
131 devices.remove(device.getAddress());
133 discoveryListeners.forEach(listener -> listener.deviceRemoved(device));
136 private boolean shouldRemove(BD device) {
137 // we can't remove devices with listeners since that means they have a handler.
138 if (device.hasListeners()) {
141 // devices that are connected won't receive any scan notifications so we can't remove them for being idle
142 if (device.getConnectionState() == ConnectionState.CONNECTED) {
146 ZonedDateTime lastActiveTime = device.getLastSeenTime();
147 if (lastActiveTime == null) {
148 // we want any new device to at least live a certain amount of time so it has a chance to be discovered or
150 lastActiveTime = device.createTime;
152 // we remove devices we haven't seen in a while
153 return ZonedDateTime.now().minusSeconds(config.inactiveDeviceCleanupThreshold).isAfter(lastActiveTime);
157 public void addDiscoveryListener(BluetoothDiscoveryListener listener) {
158 discoveryListeners.add(listener);
162 public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) {
163 discoveryListeners.remove(listener);
167 public void scanStart() {
168 // Enable scanning even while discovery is disabled in config. This allows manual starting discovery.
169 activeScanEnabled = true;
170 refreshDiscoveredDevices();
173 protected void refreshDiscoveredDevices() {
174 logger.debug("Refreshing Bluetooth device list...");
175 synchronized (devices) {
176 devices.values().forEach(this::deviceDiscovered);
181 public void scanStop() {
182 // Set active discovery state back to the configured discovery state.
183 activeScanEnabled = false;
184 // We need to keep the adapter in discovery mode as we otherwise won't get any RSSI updates either
188 public BD getDevice(BluetoothAddress address) {
189 synchronized (devices) {
190 return Objects.requireNonNull(devices.computeIfAbsent(address, this::createDevice));
194 protected abstract BD createDevice(BluetoothAddress address);
197 public boolean hasHandlerForDevice(BluetoothAddress address) {
198 String addrStr = address.toString();
200 * This type of search is inefficient and won't scale as the number of bluetooth Thing children increases on
201 * this bridge. But implementing a more efficient search would require a bit more overhead.
202 * Luckily though, it is reasonable to assume that the number of Thing children will remain small.
204 for (Thing childThing : getThing().getThings()) {
205 Object childAddr = childThing.getConfiguration().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS);
206 if (addrStr.equals(childAddr)) {
207 return childThing.getHandler() != null;
213 public void deviceDiscovered(BluetoothDevice device) {
214 if (hasHandlerForDevice(device.getAddress())) {
215 // no point in discovering a device that already has a handler
218 if (config.backgroundDiscovery || activeScanEnabled) {
219 if (deviceReachable(device)) {
220 discoveryListeners.forEach(listener -> listener.deviceDiscovered(device));
222 logger.trace("Not notifying listeners for device '{}', because it is not reachable.",
223 device.getAddress());
228 private boolean deviceReachable(BluetoothDevice device) {
229 Integer rssi = device.getRssi();
230 return rssi != null && rssi != 0;