]> git.basschouten.com Git - openhab-addons.git/blob
77bc22de96b387ebc94bf8ed8f2431b63835ef16
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth;
14
15 import java.time.ZonedDateTime;
16 import java.util.Map;
17 import java.util.Objects;
18 import java.util.Set;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.CopyOnWriteArraySet;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
35
36 /**
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.
39  *
40  * Subclasses will primarily be responsible for device discovery
41  *
42  * @author Connor Petty - Initial contribution from refactored code
43  */
44 @NonNullByDefault
45 public abstract class AbstractBluetoothBridgeHandler<BD extends BaseBluetoothDevice> extends BaseBridgeHandler
46         implements BluetoothAdapter {
47
48     private final Logger logger = LoggerFactory.getLogger(AbstractBluetoothBridgeHandler.class);
49
50     // Set of discovery listeners
51     private final Set<BluetoothDiscoveryListener> discoveryListeners = new CopyOnWriteArraySet<>();
52
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<>();
56
57     // Actual discovery status.
58     protected volatile boolean activeScanEnabled = false;
59
60     private BaseBluetoothBridgeHandlerConfiguration config = new BaseBluetoothBridgeHandlerConfiguration();
61
62     private @Nullable ScheduledFuture<?> inactiveRemovalJob;
63
64     /**
65      * Constructor
66      *
67      * @param bridge the bridge definition for this handler
68      */
69     public AbstractBluetoothBridgeHandler(Bridge bridge) {
70         super(bridge);
71     }
72
73     @Override
74     public ThingUID getUID() {
75         return getThing().getUID();
76     }
77
78     @Override
79     public @Nullable String getLocation() {
80         return getThing().getLocation();
81     }
82
83     @Override
84     public @Nullable String getLabel() {
85         return getThing().getLabel();
86     }
87
88     @Override
89     public void initialize() {
90         config = getConfigAs(BaseBluetoothBridgeHandlerConfiguration.class);
91
92         int intervalSecs = config.inactiveDeviceCleanupInterval;
93         inactiveRemovalJob = scheduler.scheduleWithFixedDelay(this::removeInactiveDevices, intervalSecs, intervalSecs,
94                 TimeUnit.SECONDS);
95     }
96
97     @Override
98     public void dispose() {
99         ScheduledFuture<?> inactiveRemovalJob = this.inactiveRemovalJob;
100         if (inactiveRemovalJob != null) {
101             inactiveRemovalJob.cancel(true);
102         }
103         this.inactiveRemovalJob = null;
104
105         synchronized (devices) {
106             for (BD device : devices.values()) {
107                 removeDevice(device);
108             }
109         }
110     }
111
112     @Override
113     public void handleCommand(ChannelUID channelUID, Command command) {
114     }
115
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);
123                 }
124             }
125         }
126     }
127
128     protected void removeDevice(BD device) {
129         device.dispose();
130         synchronized (devices) {
131             devices.remove(device.getAddress());
132         }
133         discoveryListeners.forEach(listener -> listener.deviceRemoved(device));
134     }
135
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()) {
139             return false;
140         }
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) {
143             return false;
144         }
145
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
149             // listened to.
150             lastActiveTime = device.createTime;
151         }
152         // we remove devices we haven't seen in a while
153         return ZonedDateTime.now().minusSeconds(config.inactiveDeviceCleanupThreshold).isAfter(lastActiveTime);
154     }
155
156     @Override
157     public void addDiscoveryListener(BluetoothDiscoveryListener listener) {
158         discoveryListeners.add(listener);
159     }
160
161     @Override
162     public void removeDiscoveryListener(@Nullable BluetoothDiscoveryListener listener) {
163         discoveryListeners.remove(listener);
164     }
165
166     @Override
167     public void scanStart() {
168         // Enable scanning even while discovery is disabled in config. This allows manual starting discovery.
169         activeScanEnabled = true;
170         refreshDiscoveredDevices();
171     }
172
173     protected void refreshDiscoveredDevices() {
174         logger.debug("Refreshing Bluetooth device list...");
175         synchronized (devices) {
176             devices.values().forEach(this::deviceDiscovered);
177         }
178     }
179
180     @Override
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
185     }
186
187     @Override
188     public BD getDevice(BluetoothAddress address) {
189         synchronized (devices) {
190             return Objects.requireNonNull(devices.computeIfAbsent(address, this::createDevice));
191         }
192     }
193
194     protected abstract BD createDevice(BluetoothAddress address);
195
196     @Override
197     public boolean hasHandlerForDevice(BluetoothAddress address) {
198         String addrStr = address.toString();
199         /*
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.
203          */
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;
208             }
209         }
210         return false;
211     }
212
213     public void deviceDiscovered(BluetoothDevice device) {
214         if (hasHandlerForDevice(device.getAddress())) {
215             // no point in discovering a device that already has a handler
216             return;
217         }
218         if (config.backgroundDiscovery || activeScanEnabled) {
219             if (deviceReachable(device)) {
220                 discoveryListeners.forEach(listener -> listener.deviceDiscovered(device));
221             } else {
222                 logger.trace("Not notifying listeners for device '{}', because it is not reachable.",
223                         device.getAddress());
224             }
225         }
226     }
227
228     private boolean deviceReachable(BluetoothDevice device) {
229         Integer rssi = device.getRssi();
230         return rssi != null && rssi != 0;
231     }
232 }