]> git.basschouten.com Git - openhab-addons.git/blob
6fb93c81147dc02fe08120f58ead74e2a1b1929b
[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.ArrayList;
17 import java.util.List;
18 import java.util.concurrent.locks.ReentrantLock;
19
20 import javax.measure.quantity.Power;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
25 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
26 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
27 import org.openhab.core.library.types.QuantityType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.library.unit.Units;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.thing.binding.BridgeHandler;
38 import org.openhab.core.thing.binding.builder.ChannelBuilder;
39 import org.openhab.core.thing.binding.builder.ThingBuilder;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openhab.core.types.UnDefType;
43
44 /**
45  * This is a handler for generic Bluetooth devices in beacon-mode (i.e. not connected), which at the same time can be
46  * used as a base implementation for more specific thing handlers.
47  *
48  * @author Kai Kreuzer - Initial contribution and API
49  */
50 @NonNullByDefault
51 public class BeaconBluetoothHandler extends BaseThingHandler implements BluetoothDeviceListener {
52
53     @NonNullByDefault({} /* non-null if initialized */)
54     protected BluetoothAdapter adapter;
55
56     @NonNullByDefault({} /* non-null if initialized */)
57     protected BluetoothAddress address;
58
59     @NonNullByDefault({} /* non-null if initialized */)
60     protected BluetoothDevice device;
61
62     protected final ReentrantLock deviceLock;
63
64     private @Nullable ZonedDateTime lastActivityTime;
65
66     public BeaconBluetoothHandler(Thing thing) {
67         super(thing);
68         deviceLock = new ReentrantLock();
69     }
70
71     @Override
72     public void initialize() {
73         try {
74             address = new BluetoothAddress(getConfig().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS).toString());
75         } catch (IllegalArgumentException e) {
76             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
77             return;
78         }
79
80         Bridge bridge = getBridge();
81         if (bridge == null) {
82             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Not associated with any bridge");
83             return;
84         }
85
86         BridgeHandler bridgeHandler = bridge.getHandler();
87         if (!(bridgeHandler instanceof BluetoothAdapter)) {
88             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
89                     "Associated with an unsupported bridge");
90             return;
91         }
92
93         adapter = (BluetoothAdapter) bridgeHandler;
94
95         try {
96             deviceLock.lock();
97             device = adapter.getDevice(address);
98             device.addListener(this);
99         } finally {
100             deviceLock.unlock();
101         }
102
103         ThingBuilder builder = editThing();
104         boolean changed = false;
105         for (Channel channel : createDynamicChannels()) {
106             // we only want to add each channel, not replace all of them
107             if (getThing().getChannel(channel.getUID()) == null) {
108                 builder.withChannel(channel);
109                 changed = true;
110             }
111         }
112         if (changed) {
113             updateThing(builder.build());
114         }
115
116         updateStatus(ThingStatus.UNKNOWN);
117     }
118
119     private Channel buildChannel(String channelType, String itemType) {
120         return ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelType), itemType).build();
121     }
122
123     protected List<Channel> createDynamicChannels() {
124         List<Channel> channels = new ArrayList<>();
125         channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, "Number:Power"));
126         if (device instanceof DelegateBluetoothDevice) {
127             channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, "String"));
128             channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, "String"));
129         }
130         return channels;
131     }
132
133     @Override
134     public void dispose() {
135         try {
136             deviceLock.lock();
137             if (device != null) {
138                 device.removeListener(this);
139                 device.disconnect();
140                 device = null;
141             }
142         } finally {
143             deviceLock.unlock();
144         }
145     }
146
147     @Override
148     public void handleCommand(ChannelUID channelUID, Command command) {
149         if (command == RefreshType.REFRESH) {
150             switch (channelUID.getId()) {
151                 case BluetoothBindingConstants.CHANNEL_TYPE_RSSI:
152                     updateRSSI();
153                     break;
154                 case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER:
155                     updateAdapter();
156                     break;
157                 case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION:
158                     updateAdapterLocation();
159                     break;
160             }
161         }
162     }
163
164     /**
165      * Updates the RSSI channel and the Thing status according to the new received rssi value
166      */
167     protected void updateRSSI() {
168         if (device != null) {
169             updateRSSI(device.getRssi());
170         }
171     }
172
173     private void updateRSSI(@Nullable Integer rssi) {
174         if (rssi != null && rssi != 0) {
175             QuantityType<Power> quantity = new QuantityType<>(rssi, Units.DECIBEL_MILLIWATTS);
176             updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, quantity);
177             updateStatusBasedOnRssi(true);
178         } else {
179             updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, UnDefType.NULL);
180             updateStatusBasedOnRssi(false);
181         }
182     }
183
184     protected void updateAdapter() {
185         if (device != null) {
186             BluetoothAdapter adapter = device.getAdapter();
187             updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, new StringType(adapter.getUID().getId()));
188         }
189     }
190
191     protected void updateAdapterLocation() {
192         if (device != null) {
193             BluetoothAdapter adapter = device.getAdapter();
194             String location = adapter.getLocation();
195             if (location != null && !location.isBlank()) {
196                 updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, new StringType(location));
197             } else {
198                 updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, UnDefType.NULL);
199             }
200         }
201     }
202
203     /**
204      * This method sets the Thing status based on whether or not we can receive a signal from it.
205      * This is the best logic for beacons, but connected devices might want to deactivate this by overriding the method.
206      *
207      * @param receivedSignal true, if the device is in reach
208      */
209     protected void updateStatusBasedOnRssi(boolean receivedSignal) {
210         if (receivedSignal) {
211             updateStatus(ThingStatus.ONLINE);
212         } else {
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
214         }
215     }
216
217     protected void onActivity() {
218         this.lastActivityTime = ZonedDateTime.now();
219     }
220
221     @Override
222     public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
223         onActivity();
224         int rssi = scanNotification.getRssi();
225         if (rssi != Integer.MIN_VALUE) {
226             updateRSSI(rssi);
227         } else {
228             // we received a scan notification from this device so it is online
229             // TODO how can we detect if the underlying bluez stack is still receiving advertising packets when there
230             // are no changes?
231             updateStatus(ThingStatus.ONLINE);
232         }
233     }
234
235     @Override
236     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
237         // a disconnection doesn't count as activity
238         if (connectionNotification.getConnectionState() != ConnectionState.DISCONNECTED) {
239             onActivity();
240         }
241     }
242
243     @Override
244     public void onServicesDiscovered() {
245         onActivity();
246     }
247
248     @Override
249     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
250         onActivity();
251     }
252
253     @Override
254     public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor, byte[] value) {
255         onActivity();
256     }
257
258     @Override
259     public void onAdapterChanged(BluetoothAdapter adapter) {
260         updateAdapter();
261         updateAdapterLocation();
262     }
263 }