2 * Copyright (c) 2010-2021 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 abstract public 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() {
109 sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
113 private synchronized void execute() {
114 ConnectionState connectionState = device.getConnectionState();
115 logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
118 switch (connectionState) {
122 if (isTimeToRead()) {
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);
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");
146 logger.debug("Connected to device {}", address);
147 errorConnectCounter = 0;
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);
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");
164 logger.debug("Disconnected from device {}", address);
165 errorDisconnectCounter = 0;
169 private void read() {
170 switch (serviceState) {
172 logger.debug("Discover services on device {}", address);
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) -> {
186 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
188 updateStatus(ThingStatus.ONLINE);
189 sinceLastReadSec.set(0);
190 updateChannels(BluetoothUtils.toIntArray(data));
192 readState = ReadState.IDLE;
198 if (errorReadCounter < 6) {
199 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
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");
211 logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
215 default: // serviceState RESOLVING
216 errorResolvingCounter++;
217 if (errorResolvingCounter < 6) {
218 logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
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");
229 private void discoverServices() {
230 logger.debug("Discover services for device {}", address);
231 serviceState = ServiceState.RESOLVING;
232 device.discoverServices();
236 public void onServicesDiscovered() {
237 serviceState = ServiceState.RESOLVED;
238 logger.debug("Service discovery completed for device {}", address);
243 private void printServices() {
244 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
248 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
249 logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
250 switch (connectionNotification.getConnectionState()) {
252 if (serviceState == ServiceState.RESOLVING) {
253 serviceState = ServiceState.NOT_RESOLVED;
255 readState = ReadState.IDLE;
264 private boolean isTimeToRead() {
265 int sinceLastRead = sinceLastReadSec.get();
266 logger.debug("Time since last update: {} sec", sinceLastRead);
267 return sinceLastRead >= refreshInterval;
271 * Provides the UUID of the characteristic, which holds the sensor data
273 * @return the UUID of the data characteristic
275 protected abstract UUID getDataUUID();
278 * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
280 * @param is the content of the bluetooth characteristic
282 abstract protected void updateChannels(int[] is);