]> git.basschouten.com Git - openhab-addons.git/blob
3ef40ece63cb764c7d76c02615724f0d1ed8204f
[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.airthings.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 AbstractAirthingsHandler} is responsible for handling commands, which are
36  * sent to one of the channels.
37  *
38  * @author Pauli Anttila - Initial contribution
39  * @author Kai Kreuzer - Added Airthings Wave Mini support
40  */
41 @NonNullByDefault
42 public abstract class AbstractAirthingsHandler extends BeaconBluetoothHandler {
43
44     private static final int CHECK_PERIOD_SEC = 10;
45
46     private final Logger logger = LoggerFactory.getLogger(AbstractAirthingsHandler.class);
47
48     private AtomicInteger sinceLastReadSec = new AtomicInteger();
49     private Optional<AirthingsConfiguration> configuration = Optional.empty();
50     private @Nullable ScheduledFuture<?> scheduledTask;
51
52     private volatile int refreshInterval;
53     private volatile int errorConnectCounter;
54     private volatile int errorReadCounter;
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     }
71
72     public AbstractAirthingsHandler(Thing thing) {
73         super(thing);
74     }
75
76     @Override
77     public void initialize() {
78         logger.debug("Initialize");
79         super.initialize();
80         configuration = Optional.of(getConfigAs(AirthingsConfiguration.class));
81         logger.debug("Using configuration: {}", configuration.get());
82         cancelScheduledTask();
83         configuration.ifPresent(cfg -> {
84             refreshInterval = cfg.refreshInterval;
85             logger.debug("Start scheduled task to read device in every {} seconds", refreshInterval);
86             scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
87                     TimeUnit.SECONDS);
88         });
89         sinceLastReadSec.set(refreshInterval); // update immediately
90     }
91
92     @Override
93     public void dispose() {
94         logger.debug("Dispose");
95         cancelScheduledTask();
96         serviceState = ServiceState.NOT_RESOLVED;
97         readState = ReadState.IDLE;
98         super.dispose();
99     }
100
101     private void cancelScheduledTask() {
102         if (scheduledTask != null) {
103             scheduledTask.cancel(true);
104             scheduledTask = null;
105         }
106     }
107
108     private void executePeridioc() {
109         try {
110             sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
111             execute();
112         } catch (Exception e) { // catch all to avoid scheduleWithFixedDelay being suppressed
113             logger.warn("Failed to read Airthings device", e);
114         }
115     }
116
117     private synchronized void execute() {
118         ConnectionState connectionState = device.getConnectionState();
119         logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
120                 readState);
121
122         switch (connectionState) {
123             case DISCOVERING:
124             case DISCOVERED:
125             case DISCONNECTED:
126                 if (isTimeToRead()) {
127                     connect();
128                 }
129                 break;
130             case CONNECTED:
131                 read();
132                 break;
133             default:
134                 break;
135         }
136     }
137
138     private void connect() {
139         logger.debug("Connect to device {}...", address);
140         if (!device.connect()) {
141             errorConnectCounter++;
142             if (errorConnectCounter < 6) {
143                 logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
144             } else {
145                 logger.debug("ERROR:  Controller reset needed.  Connecting to device {} failed {} times", address,
146                         errorConnectCounter);
147                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
148             }
149         } else {
150             logger.debug("Connected to device {}", address);
151             errorConnectCounter = 0;
152         }
153     }
154
155     private void disconnect() {
156         logger.debug("Disconnect from device {}...", address);
157         if (!device.disconnect()) {
158             errorDisconnectCounter++;
159             if (errorDisconnectCounter < 6) {
160                 logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
161             } else {
162                 logger.debug("ERROR:  Controller reset needed.  Disconnect from device {} failed {} times", address,
163                         errorDisconnectCounter);
164                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
165                         "Disconnect from device failed");
166             }
167         } else {
168             logger.debug("Disconnected from device {}", address);
169             errorDisconnectCounter = 0;
170         }
171     }
172
173     private void read() {
174         switch (serviceState) {
175             case NOT_RESOLVED:
176                 logger.debug("Discover services on device {}", address);
177                 discoverServices();
178                 break;
179             case RESOLVED:
180                 switch (readState) {
181                     case IDLE:
182                         logger.debug("Read data from device {}...", address);
183                         BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
184                         if (characteristic != null) {
185                             readState = ReadState.READING;
186                             errorReadCounter = 0;
187                             errorResolvingCounter = 0;
188                             device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
189                                 try {
190                                     logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
191                                             address, data);
192                                     updateStatus(ThingStatus.ONLINE);
193                                     sinceLastReadSec.set(0);
194                                     updateChannels(BluetoothUtils.toIntArray(data));
195                                 } finally {
196                                     readState = ReadState.IDLE;
197                                     disconnect();
198                                 }
199                             });
200                         } else {
201                             errorReadCounter++;
202                             if (errorReadCounter < 6) {
203                                 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
204                             } else {
205                                 logger.debug(
206                                         "ERROR:  Controller reset needed.  Read data from device {} failed {} times",
207                                         address, errorReadCounter);
208                                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
209                                         "Read data from device failed");
210                             }
211                             disconnect();
212                         }
213                         break;
214                     default:
215                         logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
216                         break;
217                 }
218                 break;
219             default: // serviceState RESOLVING
220                 errorResolvingCounter++;
221                 if (errorResolvingCounter < 6) {
222                     logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
223                 } else {
224                     logger.debug("ERROR:  Controller reset needed.  Unhandled serviceState {} on device {}",
225                             serviceState, address);
226                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
227                             "Service discovery for device failed");
228                 }
229                 break;
230         }
231     }
232
233     private void discoverServices() {
234         logger.debug("Discover services for device {}", address);
235         serviceState = ServiceState.RESOLVING;
236         device.discoverServices();
237     }
238
239     @Override
240     public void onServicesDiscovered() {
241         serviceState = ServiceState.RESOLVED;
242         logger.debug("Service discovery completed for device {}", address);
243         printServices();
244         execute();
245     }
246
247     private void printServices() {
248         device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
249     }
250
251     @Override
252     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
253         logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
254         switch (connectionNotification.getConnectionState()) {
255             case DISCONNECTED:
256                 if (serviceState == ServiceState.RESOLVING) {
257                     serviceState = ServiceState.NOT_RESOLVED;
258                 }
259                 readState = ReadState.IDLE;
260                 break;
261             default:
262                 break;
263
264         }
265         execute();
266     }
267
268     private boolean isTimeToRead() {
269         int sinceLastRead = sinceLastReadSec.get();
270         logger.debug("Time since last update: {} sec", sinceLastRead);
271         return sinceLastRead >= refreshInterval;
272     }
273
274     /**
275      * Provides the UUID of the characteristic, which holds the sensor data
276      *
277      * @return the UUID of the data characteristic
278      */
279     protected abstract UUID getDataUUID();
280
281     /**
282      * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
283      *
284      * @param is the content of the bluetooth characteristic
285      */
286     protected abstract void updateChannels(int[] is);
287 }