]> git.basschouten.com Git - openhab-addons.git/blob
4d3d89c3de1f4738d4c8d3d079890e71d0e21912
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 abstract public 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         sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
110         execute();
111     }
112
113     private synchronized void execute() {
114         ConnectionState connectionState = device.getConnectionState();
115         logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
116                 readState);
117
118         switch (connectionState) {
119             case DISCOVERING:
120             case DISCOVERED:
121             case DISCONNECTED:
122                 if (isTimeToRead()) {
123                     connect();
124                 }
125                 break;
126             case CONNECTED:
127                 read();
128                 break;
129             default:
130                 break;
131         }
132     }
133
134     private void connect() {
135         logger.debug("Connect to device {}...", address);
136         if (!device.connect()) {
137             errorConnectCounter++;
138             if (errorConnectCounter < 6) {
139                 logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
140             } else {
141                 logger.debug("ERROR:  Controller reset needed.  Connecting to device {} failed {} times", address,
142                         errorConnectCounter);
143                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
144             }
145         } else {
146             logger.debug("Connected to device {}", address);
147             errorConnectCounter = 0;
148         }
149     }
150
151     private void disconnect() {
152         logger.debug("Disconnect from device {}...", address);
153         if (!device.disconnect()) {
154             errorDisconnectCounter++;
155             if (errorDisconnectCounter < 6) {
156                 logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
157             } else {
158                 logger.debug("ERROR:  Controller reset needed.  Disconnect from device {} failed {} times", address,
159                         errorDisconnectCounter);
160                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
161                         "Disconnect from device failed");
162             }
163         } else {
164             logger.debug("Disconnected from device {}", address);
165             errorDisconnectCounter = 0;
166         }
167     }
168
169     private void read() {
170         switch (serviceState) {
171             case NOT_RESOLVED:
172                 logger.debug("Discover services on device {}", address);
173                 discoverServices();
174                 break;
175             case RESOLVED:
176                 switch (readState) {
177                     case IDLE:
178                         logger.debug("Read data from device {}...", address);
179                         BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
180                         if (characteristic != null) {
181                             readState = ReadState.READING;
182                             errorReadCounter = 0;
183                             errorResolvingCounter = 0;
184                             device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
185                                 try {
186                                     logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
187                                             address, data);
188                                     updateStatus(ThingStatus.ONLINE);
189                                     sinceLastReadSec.set(0);
190                                     updateChannels(BluetoothUtils.toIntArray(data));
191                                 } finally {
192                                     readState = ReadState.IDLE;
193                                     disconnect();
194                                 }
195                             });
196                         } else {
197                             errorReadCounter++;
198                             if (errorReadCounter < 6) {
199                                 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
200                             } else {
201                                 logger.debug(
202                                         "ERROR:  Controller reset needed.  Read data from device {} failed {} times",
203                                         address, errorReadCounter);
204                                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
205                                         "Read data from device failed");
206                             }
207                             disconnect();
208                         }
209                         break;
210                     default:
211                         logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
212                         break;
213                 }
214                 break;
215             default: // serviceState RESOLVING
216                 errorResolvingCounter++;
217                 if (errorResolvingCounter < 6) {
218                     logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
219                 } else {
220                     logger.debug("ERROR:  Controller reset needed.  Unhandled serviceState {} on device {}",
221                             serviceState, address);
222                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
223                             "Service discovery for device failed");
224                 }
225                 break;
226         }
227     }
228
229     private void discoverServices() {
230         logger.debug("Discover services for device {}", address);
231         serviceState = ServiceState.RESOLVING;
232         device.discoverServices();
233     }
234
235     @Override
236     public void onServicesDiscovered() {
237         serviceState = ServiceState.RESOLVED;
238         logger.debug("Service discovery completed for device {}", address);
239         printServices();
240         execute();
241     }
242
243     private void printServices() {
244         device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
245     }
246
247     @Override
248     public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
249         logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
250         switch (connectionNotification.getConnectionState()) {
251             case DISCONNECTED:
252                 if (serviceState == ServiceState.RESOLVING) {
253                     serviceState = ServiceState.NOT_RESOLVED;
254                 }
255                 readState = ReadState.IDLE;
256                 break;
257             default:
258                 break;
259
260         }
261         execute();
262     }
263
264     private boolean isTimeToRead() {
265         int sinceLastRead = sinceLastReadSec.get();
266         logger.debug("Time since last update: {} sec", sinceLastRead);
267         return sinceLastRead >= refreshInterval;
268     }
269
270     /**
271      * Provides the UUID of the characteristic, which holds the sensor data
272      *
273      * @return the UUID of the data characteristic
274      */
275     protected abstract UUID getDataUUID();
276
277     /**
278      * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
279      *
280      * @param is the content of the bluetooth characteristic
281      */
282     abstract protected void updateChannels(int[] is);
283 }