]> git.basschouten.com Git - openhab-addons.git/blob
370e3bc2dea436edee41a8991696fb65e935fa66
[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.util.Set;
16 import java.util.concurrent.CopyOnWriteArraySet;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19
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;
41
42 /**
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.
45  *
46  * @author Kai Kreuzer - Initial contribution and API
47  *
48  */
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 {
52
53     private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
54     private ScheduledFuture<?> connectionJob;
55
56     // internal flag for the service resolution status
57     protected volatile boolean resolved = false;
58
59     protected final Set<BluetoothCharacteristic> deviceCharacteristics = new CopyOnWriteArraySet<>();
60
61     public ConnectedBluetoothHandler(Thing thing) {
62         super(thing);
63     }
64
65     @Override
66     public void initialize() {
67         super.initialize();
68
69         connectionJob = scheduler.scheduleWithFixedDelay(() -> {
70             if (device.getConnectionState() != ConnectionState.CONNECTED) {
71                 device.connect();
72                 // we do not set the Thing status here, because we will anyhow receive a call to onConnectionStateChange
73             }
74             updateRSSI();
75         }, 0, 30, TimeUnit.SECONDS);
76     }
77
78     @Override
79     public void dispose() {
80         if (connectionJob != null) {
81             connectionJob.cancel(true);
82             connectionJob = null;
83         }
84         scheduler.submit(() -> {
85             try {
86                 deviceLock.lock();
87                 if (device != null) {
88                     device.removeListener(this);
89                     device.disconnect();
90                     device = null;
91                 }
92             } finally {
93                 deviceLock.unlock();
94             }
95         });
96     }
97
98     @Override
99     public void handleCommand(ChannelUID channelUID, Command command) {
100         super.handleCommand(channelUID, command);
101
102         // Handle REFRESH
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);
108                     break;
109                 }
110             }
111         }
112     }
113
114     @Override
115     public void channelLinked(ChannelUID channelUID) {
116         super.channelLinked(channelUID);
117     }
118
119     @Override
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
122         // we are connected.
123         if (receivedSignal) {
124             if (device.getConnectionState() == ConnectionState.CONNECTED) {
125                 updateStatus(ThingStatus.ONLINE);
126             } else {
127                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
128             }
129         } else {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
131         }
132     }
133
134     @Override
135     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
136         super.onConnectionStateChange(connectionNotification);
137         switch (connectionNotification.getConnectionState()) {
138             case DISCOVERED:
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.");
144                         }
145                     }
146                 });
147                 break;
148             case CONNECTED:
149                 updateStatus(ThingStatus.ONLINE);
150                 scheduler.submit(() -> {
151                     if (!resolved && !device.discoverServices()) {
152                         logger.debug("Error while discovering services");
153                     }
154                 });
155                 break;
156             case DISCONNECTED:
157                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
158                 break;
159             default:
160                 break;
161         }
162     }
163
164     @Override
165     public void onServicesDiscovered() {
166         super.onServicesDiscovered();
167         if (!resolved) {
168             resolved = true;
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());
175             }
176         }
177     }
178
179     @Override
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);
185             } else {
186                 if (logger.isDebugEnabled()) {
187                     logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(),
188                             address, HexUtils.bytesToHex(characteristic.getByteValue()));
189                 }
190             }
191         } else {
192             logger.debug("Characteristic {} from {} has been read - ERROR", characteristic.getUuid(), address);
193         }
194     }
195
196     @Override
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);
203         }
204     }
205
206     @Override
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);
212         }
213         if (GattCharacteristic.BATTERY_LEVEL.equals(characteristic.getGattCharacteristic())) {
214             updateBatteryLevel(characteristic);
215         }
216     }
217
218     @Override
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);
224         }
225     }
226
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()));
231     }
232
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();
240             }
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());
249             }
250             deviceCharacteristics.add(characteristic);
251             device.enableNotifications(characteristic);
252             if (isLinked(channelId)) {
253                 device.readCharacteristic(characteristic);
254             }
255         } else {
256             logger.debug("Characteristic is null - not activating any channel.");
257         }
258     }
259
260     protected void activateChannel(@Nullable BluetoothCharacteristic characteristic, ChannelTypeUID channelTypeUID) {
261         activateChannel(characteristic, channelTypeUID, null);
262     }
263 }