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