2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.airthings.internal;
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;
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;
35 * The {@link AbstractAirthingsHandler} is responsible for handling commands, which are
36 * sent to one of the channels.
38 * @author Pauli Anttila - Initial contribution
39 * @author Kai Kreuzer - Added Airthings Wave Mini support
42 public abstract class AbstractAirthingsHandler extends BeaconBluetoothHandler {
44 private static final int CHECK_PERIOD_SEC = 10;
46 private final Logger logger = LoggerFactory.getLogger(AbstractAirthingsHandler.class);
48 private AtomicInteger sinceLastReadSec = new AtomicInteger();
49 private Optional<AirthingsConfiguration> configuration = Optional.empty();
50 private @Nullable ScheduledFuture<?> scheduledTask;
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;
58 private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
59 private volatile ReadState readState = ReadState.IDLE;
61 private enum ServiceState {
67 private enum ReadState {
72 public AbstractAirthingsHandler(Thing thing) {
77 public void initialize() {
78 logger.debug("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,
89 sinceLastReadSec.set(refreshInterval); // update immediately
93 public void dispose() {
94 logger.debug("Dispose");
95 cancelScheduledTask();
96 serviceState = ServiceState.NOT_RESOLVED;
97 readState = ReadState.IDLE;
101 private void cancelScheduledTask() {
102 if (scheduledTask != null) {
103 scheduledTask.cancel(true);
104 scheduledTask = null;
108 private void executePeridioc() {
110 sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
112 } catch (Exception e) { // catch all to avoid scheduleWithFixedDelay being suppressed
113 logger.warn("Failed to read Airthings device", e);
117 private synchronized void execute() {
118 ConnectionState connectionState = device.getConnectionState();
119 logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
122 switch (connectionState) {
126 if (isTimeToRead()) {
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);
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");
150 logger.debug("Connected to device {}", address);
151 errorConnectCounter = 0;
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);
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");
168 logger.debug("Disconnected from device {}", address);
169 errorDisconnectCounter = 0;
173 private void read() {
174 switch (serviceState) {
176 logger.debug("Discover services on device {}", address);
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) -> {
190 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
192 updateStatus(ThingStatus.ONLINE);
193 sinceLastReadSec.set(0);
194 updateChannels(BluetoothUtils.toIntArray(data));
196 readState = ReadState.IDLE;
202 if (errorReadCounter < 6) {
203 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
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");
215 logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
219 default: // serviceState RESOLVING
220 errorResolvingCounter++;
221 if (errorResolvingCounter < 6) {
222 logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
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");
233 private void discoverServices() {
234 logger.debug("Discover services for device {}", address);
235 serviceState = ServiceState.RESOLVING;
236 device.discoverServices();
240 public void onServicesDiscovered() {
241 serviceState = ServiceState.RESOLVED;
242 logger.debug("Service discovery completed for device {}", address);
247 private void printServices() {
248 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
252 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
253 logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
254 switch (connectionNotification.getConnectionState()) {
256 if (serviceState == ServiceState.RESOLVING) {
257 serviceState = ServiceState.NOT_RESOLVED;
259 readState = ReadState.IDLE;
268 private boolean isTimeToRead() {
269 int sinceLastRead = sinceLastReadSec.get();
270 logger.debug("Time since last update: {} sec", sinceLastRead);
271 return sinceLastRead >= refreshInterval;
275 * Provides the UUID of the characteristic, which holds the sensor data
277 * @return the UUID of the data characteristic
279 protected abstract UUID getDataUUID();
282 * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
284 * @param is the content of the bluetooth characteristic
286 protected abstract void updateChannels(int[] is);