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.BluetoothDevice.ConnectionState;
28 import org.openhab.binding.bluetooth.BluetoothUtils;
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);
160 if (characteristic != null) {
161 readState = ReadState.READING;
162 device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
165 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
167 updateStatus(ThingStatus.ONLINE);
168 sinceLastReadSec.set(0);
171 new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data)));
172 } catch (AirthingsParserException e) {
174 "Data parsing error occured, when parsing data from device {}, cause {}",
175 address, e.getMessage(), e);
178 logger.debug("Characteristic {} from device {} failed: {}",
179 characteristic.getUuid(), address, ex.getMessage());
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
184 readState = ReadState.IDLE;
189 logger.debug("Read data from device {} failed", address);
201 private void discoverServices() {
202 logger.debug("Discover services for device {}", address);
203 serviceState = ServiceState.RESOLVING;
204 device.discoverServices();
208 public void onServicesDiscovered() {
209 serviceState = ServiceState.RESOLVED;
210 logger.debug("Service discovery completed for device {}", address);
215 private void printServices() {
216 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
220 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
221 switch (connectionNotification.getConnectionState()) {
223 if (serviceState == ServiceState.RESOLVING) {
224 serviceState = ServiceState.NOT_RESOLVED;
226 readState = ReadState.IDLE;
235 private void updateChannels(AirthingsWavePlusDataParser parser) {
236 logger.debug("Parsed data: {}", parser);
237 updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));
238 updateState(CHANNEL_ID_TEMPERATURE,
239 QuantityType.valueOf(Double.valueOf(parser.getTemperature()), SIUnits.CELSIUS));
240 updateState(CHANNEL_ID_PRESSURE, QuantityType.valueOf(Double.valueOf(parser.getPressure()), Units.MILLIBAR));
241 updateState(CHANNEL_ID_CO2, QuantityType.valueOf(Double.valueOf(parser.getCo2()), Units.PARTS_PER_MILLION));
242 updateState(CHANNEL_ID_TVOC, QuantityType.valueOf(Double.valueOf(parser.getTvoc()), PARTS_PER_BILLION));
243 updateState(CHANNEL_ID_RADON_ST_AVG,
244 QuantityType.valueOf(Double.valueOf(parser.getRadonShortTermAvg()), BECQUEREL_PER_CUBIC_METRE));
245 updateState(CHANNEL_ID_RADON_LT_AVG,
246 QuantityType.valueOf(Double.valueOf(parser.getRadonLongTermAvg()), BECQUEREL_PER_CUBIC_METRE));
249 private boolean isTimeToRead() {
250 int sinceLastRead = sinceLastReadSec.get();
251 logger.debug("Time since last update: {} sec", sinceLastRead);
252 return sinceLastRead >= refreshInterval;