2 * Copyright (c) 2010-2021 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;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.concurrent.locks.ReentrantLock;
20 import javax.measure.quantity.Power;
22 import org.apache.commons.lang.StringUtils;
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.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
27 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
28 import org.openhab.core.library.types.QuantityType;
29 import org.openhab.core.library.types.StringType;
30 import org.openhab.core.library.unit.Units;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.binding.BaseThingHandler;
38 import org.openhab.core.thing.binding.BridgeHandler;
39 import org.openhab.core.thing.binding.builder.ChannelBuilder;
40 import org.openhab.core.thing.binding.builder.ThingBuilder;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.UnDefType;
46 * This is a handler for generic Bluetooth devices in beacon-mode (i.e. not connected), which at the same time can be
47 * used as a base implementation for more specific thing handlers.
49 * @author Kai Kreuzer - Initial contribution and API
52 public class BeaconBluetoothHandler extends BaseThingHandler implements BluetoothDeviceListener {
54 @NonNullByDefault({} /* non-null if initialized */)
55 protected BluetoothAdapter adapter;
57 @NonNullByDefault({} /* non-null if initialized */)
58 protected BluetoothAddress address;
60 @NonNullByDefault({} /* non-null if initialized */)
61 protected BluetoothDevice device;
63 protected final ReentrantLock deviceLock;
65 private @Nullable ZonedDateTime lastActivityTime;
67 public BeaconBluetoothHandler(Thing thing) {
69 deviceLock = new ReentrantLock();
73 public void initialize() {
75 address = new BluetoothAddress(getConfig().get(BluetoothBindingConstants.CONFIGURATION_ADDRESS).toString());
76 } catch (IllegalArgumentException e) {
77 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
81 Bridge bridge = getBridge();
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Not associated with any bridge");
87 BridgeHandler bridgeHandler = bridge.getHandler();
88 if (!(bridgeHandler instanceof BluetoothAdapter)) {
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
90 "Associated with an unsupported bridge");
94 adapter = (BluetoothAdapter) bridgeHandler;
98 device = adapter.getDevice(address);
99 device.addListener(this);
104 ThingBuilder builder = editThing();
105 boolean changed = false;
106 for (Channel channel : createDynamicChannels()) {
107 // we only want to add each channel, not replace all of them
108 if (getThing().getChannel(channel.getUID()) == null) {
109 builder.withChannel(channel);
114 updateThing(builder.build());
117 updateStatus(ThingStatus.UNKNOWN);
120 private Channel buildChannel(String channelType, String itemType) {
121 return ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelType), itemType).build();
124 protected List<Channel> createDynamicChannels() {
125 List<Channel> channels = new ArrayList<>();
126 channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, "Number:Power"));
127 if (device instanceof DelegateBluetoothDevice) {
128 channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, "String"));
129 channels.add(buildChannel(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, "String"));
135 public void dispose() {
138 if (device != null) {
139 device.removeListener(this);
149 public void handleCommand(ChannelUID channelUID, Command command) {
150 if (command == RefreshType.REFRESH) {
151 switch (channelUID.getId()) {
152 case BluetoothBindingConstants.CHANNEL_TYPE_RSSI:
155 case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER:
158 case BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION:
159 updateAdapterLocation();
166 * Updates the RSSI channel and the Thing status according to the new received rssi value
168 protected void updateRSSI() {
169 if (device != null) {
170 updateRSSI(device.getRssi());
174 private void updateRSSI(@Nullable Integer rssi) {
175 if (rssi != null && rssi != 0) {
176 QuantityType<Power> quantity = new QuantityType<>(rssi, Units.DECIBEL_MILLIWATTS);
177 updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, quantity);
178 updateStatusBasedOnRssi(true);
180 updateState(BluetoothBindingConstants.CHANNEL_TYPE_RSSI, UnDefType.NULL);
181 updateStatusBasedOnRssi(false);
185 protected void updateAdapter() {
186 if (device != null) {
187 BluetoothAdapter adapter = device.getAdapter();
188 updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER, new StringType(adapter.getUID().getId()));
192 protected void updateAdapterLocation() {
193 if (device != null) {
194 BluetoothAdapter adapter = device.getAdapter();
195 String location = adapter.getLocation();
196 if (location != null || StringUtils.isBlank(location)) {
197 updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, new StringType(location));
199 updateState(BluetoothBindingConstants.CHANNEL_TYPE_ADAPTER_LOCATION, UnDefType.NULL);
205 * This method sets the Thing status based on whether or not we can receive a signal from it.
206 * This is the best logic for beacons, but connected devices might want to deactivate this by overriding the method.
208 * @param receivedSignal true, if the device is in reach
210 protected void updateStatusBasedOnRssi(boolean receivedSignal) {
211 if (receivedSignal) {
212 updateStatus(ThingStatus.ONLINE);
214 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
218 private void onActivity() {
219 this.lastActivityTime = ZonedDateTime.now();
223 public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
225 int rssi = scanNotification.getRssi();
226 if (rssi != Integer.MIN_VALUE) {
232 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
233 // a disconnection doesn't count as activity
234 if (connectionNotification.getConnectionState() != ConnectionState.DISCONNECTED) {
240 public void onServicesDiscovered() {
245 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
246 if (status == BluetoothCompletionStatus.SUCCESS) {
252 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
253 BluetoothCompletionStatus status) {
254 if (status == BluetoothCompletionStatus.SUCCESS) {
260 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
265 public void onDescriptorUpdate(BluetoothDescriptor bluetoothDescriptor) {
270 public void onAdapterChanged(BluetoothAdapter adapter) {
272 updateAdapterLocation();