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