2 * Copyright (c) 2010-2020 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;
16 import java.util.concurrent.CopyOnWriteArraySet;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.DefaultLocation;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.bluetooth.BluetoothCharacteristic.GattCharacteristic;
24 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
25 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.thing.Channel;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
30 import org.openhab.core.thing.Thing;
31 import org.openhab.core.thing.ThingStatus;
32 import org.openhab.core.thing.ThingStatusDetail;
33 import org.openhab.core.thing.binding.builder.ChannelBuilder;
34 import org.openhab.core.thing.binding.builder.ThingBuilder;
35 import org.openhab.core.thing.type.ChannelTypeUID;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.RefreshType;
38 import org.openhab.core.util.HexUtils;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * This is a handler for generic Bluetooth devices in connected mode, which at the same time can be used
44 * as a base implementation for more specific thing handlers.
46 * @author Kai Kreuzer - Initial contribution and API
49 @NonNullByDefault({ DefaultLocation.PARAMETER, DefaultLocation.RETURN_TYPE, DefaultLocation.ARRAY_CONTENTS,
50 DefaultLocation.TYPE_ARGUMENT, DefaultLocation.TYPE_BOUND, DefaultLocation.TYPE_PARAMETER })
51 public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
53 private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
54 private ScheduledFuture<?> connectionJob;
56 // internal flag for the service resolution status
57 protected volatile boolean resolved = false;
59 protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
61 public ConnectedBluetoothHandler(Thing thing) {
66 public void initialize() {
69 connectionJob = scheduler.scheduleWithFixedDelay(() -> {
70 if (device.getConnectionState() != ConnectionState.CONNECTED) {
72 // we do not set the Thing status here, because we will anyhow receive a call to onConnectionStateChange
75 }, 0, 30, TimeUnit.SECONDS);
79 public void dispose() {
80 if (connectionJob != null) {
81 connectionJob.cancel(true);
84 scheduler.submit(() -> {
88 device.removeListener(this);
99 public void handleCommand(ChannelUID channelUID, Command command) {
100 super.handleCommand(channelUID, command);
103 if (command == RefreshType.REFRESH) {
104 for (BluetoothCharacteristic characteristic : deviceCharacteristics) {
105 if (characteristic.getGattCharacteristic() != null
106 && channelUID.getId().equals(characteristic.getGattCharacteristic().name())) {
107 device.readCharacteristic(characteristic);
115 public void channelLinked(ChannelUID channelUID) {
116 super.channelLinked(channelUID);
120 protected void updateStatusBasedOnRssi(boolean receivedSignal) {
121 // if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
123 if (receivedSignal) {
124 if (device.getConnectionState() == ConnectionState.CONNECTED) {
125 updateStatus(ThingStatus.ONLINE);
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
135 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
136 super.onConnectionStateChange(connectionNotification);
137 switch (connectionNotification.getConnectionState()) {
139 // The device is now known on the Bluetooth network, so we can do something...
140 scheduler.submit(() -> {
141 if (device.getConnectionState() != ConnectionState.CONNECTED) {
142 if (!device.connect()) {
143 logger.debug("Error connecting to device after discovery.");
149 updateStatus(ThingStatus.ONLINE);
150 scheduler.submit(() -> {
151 if (!resolved && !device.discoverServices()) {
152 logger.debug("Error while discovering services");
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
165 public void onServicesDiscovered() {
166 super.onServicesDiscovered();
169 logger.debug("Service discovery completed for '{}'", address);
170 BluetoothCharacteristic characteristic = device
171 .getCharacteristic(GattCharacteristic.BATTERY_LEVEL.getUUID());
172 if (characteristic != null) {
173 activateChannel(characteristic, DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_BATTERY_LEVEL.getUID());
174 logger.debug("Added GATT characteristic '{}'", characteristic.getGattCharacteristic().name());
180 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
181 super.onCharacteristicReadComplete(characteristic, status);
182 if (status == BluetoothCompletionStatus.SUCCESS) {
183 if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
184 updateBatteryLevel(characteristic);
186 if (logger.isDebugEnabled()) {
187 logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(),
188 address, HexUtils.bytesToHex(characteristic.getByteValue()));
192 logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
197 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
198 BluetoothCompletionStatus status) {
199 super.onCharacteristicWriteComplete(characteristic, status);
200 if (logger.isDebugEnabled()) {
201 logger.debug("Wrote {} to characteristic {} of device {}: {}",
202 HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address, status);
207 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
208 super.onCharacteristicUpdate(characteristic);
209 if (logger.isDebugEnabled()) {
210 logger.debug("Recieved update {} to characteristic {} of device {}",
211 HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
213 if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
214 updateBatteryLevel(characteristic);
219 public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
220 super.onDescriptorUpdate(descriptor);
221 if (logger.isDebugEnabled()) {
222 logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
223 descriptor.getUuid(), address);
227 protected void updateBatteryLevel(BluetoothCharacteristic characteristic) {
228 // the byte has values from 0-255, which we need to map to 0-100
229 Double level = characteristic.getValue()[0] / 2.55;
230 updateState(characteristic.getGattCharacteristic().name(), new DecimalType(level.intValue()));
233 protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID,
234 @Nullable String name) {
235 if (characteristic != null) {
236 String channelId = name != null ? name : characteristic.getGattCharacteristic().name();
237 if (channelId == null) {
238 // use the type id as a fallback
239 channelId = channelTypeUID.getId();
241 if (getThing().getChannel(channelId) == null) {
242 // the channel does not exist yet, so let's add it
243 ThingBuilder updatedThing = editThing();
244 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), channelId), "Number")
245 .withType(channelTypeUID).build();
246 updatedThing.withChannel(channel);
247 updateThing(updatedThing.build());
248 logger.debug("Added channel '{}' to Thing '{}'", channelId, getThing().getUID());
250 deviceCharacteristics.add(characteristic);
251 device.enableNotifications(characteristic);
252 if (isLinked(channelId)) {
253 device.readCharacteristic(characteristic);
256 logger.debug("Characteristic is null - not activating any channel.");
260 protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID) {
261 activateChannel(characteristic, channelTypeUID, null);