]> git.basschouten.com Git - openhab-addons.git/blob
0b75c50fda023eaffe3718baab9c6c4d6ad0d379
[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.radoneye.internal;
14
15 import java.util.UUID;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.atomic.AtomicInteger;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
23 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
24 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
25 import org.openhab.binding.bluetooth.BluetoothUtils;
26 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
27 import org.openhab.core.thing.Thing;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * The {@link AbstractRadoneyeHandler} is responsible for handling commands, which are
35  * sent to one of the channels.
36  *
37  * @author Peter Obel - Initial contribution
38  */
39 @NonNullByDefault
40 public abstract class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
41
42     private static final int CHECK_PERIOD_SEC = 10;
43
44     private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
45
46     private AtomicInteger sinceLastReadSec = new AtomicInteger();
47     private RadoneyeConfiguration configuration = new RadoneyeConfiguration();
48     private @Nullable ScheduledFuture<?> scheduledTask;
49
50     private volatile int errorConnectCounter;
51     private volatile int errorReadCounter;
52     private volatile int errorWriteCounter;
53     private volatile int errorDisconnectCounter;
54     private volatile int errorResolvingCounter;
55
56     private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
57     private volatile ReadState readState = ReadState.IDLE;
58
59     private enum ServiceState {
60         NOT_RESOLVED,
61         RESOLVING,
62         RESOLVED
63     }
64
65     private enum ReadState {
66         IDLE,
67         READING,
68         WRITING
69     }
70
71     public AbstractRadoneyeHandler(Thing thing) {
72         super(thing);
73     }
74
75     @Override
76     public void initialize() {
77         logger.debug("Initialize");
78         super.initialize();
79         configuration = getConfigAs(RadoneyeConfiguration.class);
80         logger.debug("Using configuration: {}", configuration);
81         cancelScheduledTask();
82         logger.debug("Start scheduled task to read device in every {} seconds", configuration.refreshInterval);
83         scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
84                 TimeUnit.SECONDS);
85
86         sinceLastReadSec.set(configuration.refreshInterval); // update immediately
87     }
88
89     @Override
90     public void dispose() {
91         logger.debug("Dispose");
92         cancelScheduledTask();
93         serviceState = ServiceState.NOT_RESOLVED;
94         readState = ReadState.IDLE;
95         super.dispose();
96     }
97
98     private void cancelScheduledTask() {
99         if (scheduledTask != null) {
100             scheduledTask.cancel(true);
101             scheduledTask = null;
102         }
103     }
104
105     private void executePeridioc() {
106         sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
107         execute();
108     }
109
110     private synchronized void execute() {
111         ConnectionState connectionState = device.getConnectionState();
112         logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
113                 readState);
114
115         switch (connectionState) {
116             case DISCOVERING:
117             case DISCOVERED:
118             case DISCONNECTED:
119                 if (isTimeToRead()) {
120                     connect();
121                 }
122                 break;
123             case CONNECTED:
124                 read();
125                 break;
126             default:
127                 break;
128         }
129     }
130
131     private void connect() {
132         logger.debug("Connect to device {}...", address);
133         if (!device.connect()) {
134             errorConnectCounter++;
135             if (errorConnectCounter < 6) {
136                 logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
137             } else {
138                 logger.debug("ERROR:  Controller reset needed.  Connecting to device {} failed {} times", address,
139                         errorConnectCounter);
140                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
141             }
142         } else {
143             logger.debug("Connected to device {}", address);
144             errorConnectCounter = 0;
145         }
146     }
147
148     private void disconnect() {
149         logger.debug("Disconnect from device {}...", address);
150         if (!device.disconnect()) {
151             errorDisconnectCounter++;
152             if (errorDisconnectCounter < 6) {
153                 logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
154             } else {
155                 logger.debug("ERROR:  Controller reset needed.  Disconnect from device {} failed {} times", address,
156                         errorDisconnectCounter);
157                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
158                         "Disconnect from device failed");
159             }
160         } else {
161             logger.debug("Disconnected from device {}", address);
162             errorDisconnectCounter = 0;
163         }
164     }
165
166     private void read() {
167         switch (serviceState) {
168             case NOT_RESOLVED:
169                 logger.debug("Discover services on device {}", address);
170                 discoverServices();
171                 break;
172             case RESOLVED:
173                 switch (readState) {
174                     case IDLE:
175                         if (getTriggerUUID() != null) {
176                             logger.debug("Send trigger data to device {}...", address);
177                             BluetoothCharacteristic characteristic = device.getCharacteristic(getTriggerUUID());
178                             if (characteristic != null) {
179                                 readState = ReadState.WRITING;
180                                 errorWriteCounter = 0;
181                                 device.writeCharacteristic(characteristic, getTriggerData()).whenComplete((v, ex) -> {
182                                     readSensorData();
183                                 });
184                             } else {
185                                 errorWriteCounter++;
186                                 if (errorWriteCounter < 6) {
187                                     logger.debug("Read/write data from device {} failed {} times", address,
188                                             errorWriteCounter);
189                                 } else {
190                                     logger.debug(
191                                             "ERROR:  Controller reset needed.  Read/write data from device {} failed {} times",
192                                             address, errorWriteCounter);
193                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                                             "Read/write data from device failed");
195                                 }
196                                 disconnect();
197                             }
198                         } else {
199                             readSensorData();
200                         }
201
202                         break;
203                     default:
204                         logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
205                         break;
206                 }
207                 break;
208             default: // serviceState RESOLVING
209                 errorResolvingCounter++;
210                 if (errorResolvingCounter < 6) {
211                     logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
212                 } else {
213                     logger.debug("ERROR:  Controller reset needed.  Unhandled serviceState {} on device {}",
214                             serviceState, address);
215                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
216                             "Service discovery for device failed");
217                 }
218                 break;
219         }
220     }
221
222     private void readSensorData() {
223         logger.debug("Read data from device {}...", address);
224         BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
225         if (characteristic != null) {
226             readState = ReadState.READING;
227             errorReadCounter = 0;
228             errorResolvingCounter = 0;
229             device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
230                 try {
231                     logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address, data);
232                     updateStatus(ThingStatus.ONLINE);
233                     sinceLastReadSec.set(0);
234                     updateChannels(BluetoothUtils.toIntArray(data));
235                 } finally {
236                     readState = ReadState.IDLE;
237                     disconnect();
238                 }
239             });
240         } else {
241             errorReadCounter++;
242             if (errorReadCounter < 6) {
243                 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
244             } else {
245                 logger.debug("ERROR:  Controller reset needed.  Read data from device {} failed {} times", address,
246                         errorReadCounter);
247                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248                         "Read data from device failed");
249             }
250             disconnect();
251         }
252     }
253
254     private void discoverServices() {
255         logger.debug("Discover services for device {}", address);
256         serviceState = ServiceState.RESOLVING;
257         device.discoverServices();
258     }
259
260     @Override
261     public void onServicesDiscovered() {
262         serviceState = ServiceState.RESOLVED;
263         logger.debug("Service discovery completed for device {}", address);
264         printServices();
265         execute();
266     }
267
268     private void printServices() {
269         device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
270     }
271
272     @Override
273     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
274         logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
275         switch (connectionNotification.getConnectionState()) {
276             case DISCONNECTED:
277                 if (serviceState == ServiceState.RESOLVING) {
278                     serviceState = ServiceState.NOT_RESOLVED;
279                 }
280                 readState = ReadState.IDLE;
281                 break;
282             default:
283                 break;
284
285         }
286         execute();
287     }
288
289     private boolean isTimeToRead() {
290         int sinceLastRead = sinceLastReadSec.get();
291         logger.debug("Time since last update: {} sec", sinceLastRead);
292         return sinceLastRead >= configuration.refreshInterval;
293     }
294
295     /**
296      * Provides the configured major firmware version
297      *
298      * @return the major firmware version configured
299      */
300     protected int getFwVersion() {
301         return configuration.fwVersion;
302     }
303
304     /**
305      * Provides the UUID of the characteristic, which holds the sensor data
306      *
307      * @return the UUID of the data characteristic
308      */
309     protected abstract UUID getDataUUID();
310
311     /**
312      * Provides the UUID of the characteristic, that triggers and update of the sensor data
313      *
314      * @return the UUID of the data characteristic
315      */
316     protected abstract UUID getTriggerUUID();
317
318     /**
319      * Provides the data that sent to the trigger characteristic will update the sensor data
320      *
321      * @return the trigger data as an byte array
322      */
323     protected abstract byte[] getTriggerData();
324
325     /**
326      * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
327      *
328      * @param is the content of the bluetooth characteristic
329      */
330     protected abstract void updateChannels(int[] is);
331 }