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 static org.openhab.binding.bluetooth.airthings.internal.AirthingsBindingConstants.*;
17 import java.util.Optional;
18 import java.util.UUID;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21 import java.util.concurrent.atomic.AtomicInteger;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
26 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
27 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
28 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
29 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
30 import org.openhab.core.library.types.QuantityType;
31 import org.openhab.core.library.unit.SIUnits;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link AirthingsWavePlusHandler} is responsible for handling commands, which are
41 * sent to one of the channels.
43 * @author Pauli Anttila - Initial contribution
46 public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
48 private static final String DATA_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
49 private static final int CHECK_PERIOD_SEC = 10;
51 private final Logger logger = LoggerFactory.getLogger(AirthingsWavePlusHandler.class);
52 private final UUID uuid = UUID.fromString(DATA_UUID);
54 private AtomicInteger sinceLastReadSec = new AtomicInteger();
55 private Optional<AirthingsConfiguration> configuration = Optional.empty();
56 private @Nullable ScheduledFuture<?> scheduledTask;
58 private volatile int refreshInterval;
60 private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
61 private volatile ReadState readState = ReadState.IDLE;
63 private enum ServiceState {
69 private enum ReadState {
74 public AirthingsWavePlusHandler(Thing thing) {
79 public void initialize() {
80 logger.debug("Initialize");
82 configuration = Optional.of(getConfigAs(AirthingsConfiguration.class));
83 logger.debug("Using configuration: {}", configuration.get());
84 cancelScheduledTask();
85 configuration.ifPresent(cfg -> {
86 refreshInterval = cfg.refreshInterval;
87 logger.debug("Start scheduled task to read device in every {} seconds", refreshInterval);
88 scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
91 sinceLastReadSec.set(refreshInterval); // update immediately
95 public void dispose() {
96 logger.debug("Dispose");
97 cancelScheduledTask();
98 serviceState = ServiceState.NOT_RESOLVED;
99 readState = ReadState.IDLE;
103 private void cancelScheduledTask() {
104 if (scheduledTask != null) {
105 scheduledTask.cancel(true);
106 scheduledTask = null;
110 private void executePeridioc() {
111 sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
115 private synchronized void execute() {
116 ConnectionState connectionState = device.getConnectionState();
117 logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
120 switch (connectionState) {
123 if (isTimeToRead()) {
135 private void connect() {
136 logger.debug("Connect to device {}...", address);
137 if (!device.connect()) {
138 logger.debug("Connecting to device {} failed", address);
142 private void disconnect() {
143 logger.debug("Disconnect from device {}...", address);
144 if (!device.disconnect()) {
145 logger.debug("Disconnect from device {} failed", address);
149 private void read() {
150 switch (serviceState) {
157 logger.debug("Read data from device {}...", address);
158 BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
159 if (characteristic != null && device.readCharacteristic(characteristic)) {
160 readState = ReadState.READING;
162 logger.debug("Read data from device {} failed", address);
174 private void discoverServices() {
175 logger.debug("Discover services for device {}", address);
176 serviceState = ServiceState.RESOLVING;
177 device.discoverServices();
181 public void onServicesDiscovered() {
182 serviceState = ServiceState.RESOLVED;
183 logger.debug("Service discovery completed for device {}", address);
188 private void printServices() {
189 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
193 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
194 switch (connectionNotification.getConnectionState()) {
196 if (serviceState == ServiceState.RESOLVING) {
197 serviceState = ServiceState.NOT_RESOLVED;
199 readState = ReadState.IDLE;
209 public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
211 if (status == BluetoothCompletionStatus.SUCCESS) {
212 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address,
213 characteristic.getValue());
214 updateStatus(ThingStatus.ONLINE);
215 sinceLastReadSec.set(0);
217 updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue()));
218 } catch (AirthingsParserException e) {
219 logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address,
223 logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address);
224 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device");
227 readState = ReadState.IDLE;
232 private void updateChannels(AirthingsWavePlusDataParser parser) {
233 logger.debug("Parsed data: {}", parser);
234 updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));
235 updateState(CHANNEL_ID_TEMPERATURE,
236 QuantityType.valueOf(Double.valueOf(parser.getTemperature()), SIUnits.CELSIUS));
237 updateState(CHANNEL_ID_PRESSURE, QuantityType.valueOf(Double.valueOf(parser.getPressure()), Units.MILLIBAR));
238 updateState(CHANNEL_ID_CO2, QuantityType.valueOf(Double.valueOf(parser.getCo2()), Units.PARTS_PER_MILLION));
239 updateState(CHANNEL_ID_TVOC, QuantityType.valueOf(Double.valueOf(parser.getTvoc()), PARTS_PER_BILLION));
240 updateState(CHANNEL_ID_RADON_ST_AVG,
241 QuantityType.valueOf(Double.valueOf(parser.getRadonShortTermAvg()), BECQUEREL_PER_CUBIC_METRE));
242 updateState(CHANNEL_ID_RADON_LT_AVG,
243 QuantityType.valueOf(Double.valueOf(parser.getRadonLongTermAvg()), BECQUEREL_PER_CUBIC_METRE));
246 private boolean isTimeToRead() {
247 int sinceLastRead = sinceLastReadSec.get();
248 logger.debug("Time since last update: {} sec", sinceLastRead);
249 return sinceLastRead >= refreshInterval;