Skip to content

Commit

Permalink
Add support for Airthings Wave Mini
Browse files Browse the repository at this point in the history
Signed-off-by: Kai Kreuzer <[email protected]>
  • Loading branch information
kaikreuzer committed Apr 4, 2021
1 parent e2877fa commit 6a4151a
Show file tree
Hide file tree
Showing 12 changed files with 593 additions and 378 deletions.
15 changes: 11 additions & 4 deletions bundles/org.openhab.binding.bluetooth.airthings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Following thing types are supported by this extension:

| Thing Type ID | Description |
| ------------------- | ------------------------- |
| airthings_wave_plus | Airthings Wave+ |
| airthings_wave_plus | Airthings Wave Plus |
| airthings_wave_mini | Airthings Wave Mini |


## Discovery
Expand All @@ -17,7 +18,7 @@ As any other Bluetooth device, Airthings devices are discovered automatically by

## Thing Configuration

Supported configuration parameters for `Airthings Wave+` thing:
Supported configuration parameters for the things:

| Property | Type | Default | Required | Description |
|---------------------------------|---------|---------|----------|-----------------------------------------------------------------|
Expand All @@ -26,18 +27,24 @@ Supported configuration parameters for `Airthings Wave+` thing:

## Channels

Following channels are supported for `Airthings Wave+` thing:
Following channels are supported for `Airthings Wave Mini` thing:

| Channel ID | Item Type | Description |
| ------------------ | ------------------------ | ------------------------------------------- |
| temperature | Number:Temperature | The measured temperature |
| humidity | Number:Dimensionless | The measured humidity |
| tvoc | Number:Dimensionless | The measured TVOC level |

The `Airthings Wave Plus` thing has additionally the following channels:

| Channel ID | Item Type | Description |
| ------------------ | ------------------------ | ------------------------------------------- |
| pressure | Number:Pressure | The measured air pressure |
| co2 | Number:Dimensionless | The measured CO2 level |
| tvoc | Number:Dimensionless | The measured TVOC level |
| radon_st_avg | Number:Density | The measured radon short term average level |
| radon_lt_avg | Number:Density | The measured radon long term average level |


## Example

airthings.things (assuming you have a Bluetooth bridge with the ID `bluetooth:bluegiga:adapter1`:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.airthings.internal;

import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link AbstractAirthingsHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Pauli Anttila - Initial contribution
* @author Kai Kreuzer - Added Airthings Wave Mini support
*/
@NonNullByDefault
abstract public class AbstractAirthingsHandler extends BeaconBluetoothHandler {

private static final int CHECK_PERIOD_SEC = 10;

private final Logger logger = LoggerFactory.getLogger(AbstractAirthingsHandler.class);

private AtomicInteger sinceLastReadSec = new AtomicInteger();
private Optional<AirthingsConfiguration> configuration = Optional.empty();
private @Nullable ScheduledFuture<?> scheduledTask;

private volatile int refreshInterval;

private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
private volatile ReadState readState = ReadState.IDLE;

private enum ServiceState {
NOT_RESOLVED,
RESOLVING,
RESOLVED,
}

private enum ReadState {
IDLE,
READING,
}

public AbstractAirthingsHandler(Thing thing) {
super(thing);
}

@Override
public void initialize() {
logger.debug("Initialize");
super.initialize();
configuration = Optional.of(getConfigAs(AirthingsConfiguration.class));
logger.debug("Using configuration: {}", configuration.get());
cancelScheduledTask();
configuration.ifPresent(cfg -> {
refreshInterval = cfg.refreshInterval;
logger.debug("Start scheduled task to read device in every {} seconds", refreshInterval);
scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
TimeUnit.SECONDS);
});
sinceLastReadSec.set(refreshInterval); // update immediately
}

@Override
public void dispose() {
logger.debug("Dispose");
cancelScheduledTask();
serviceState = ServiceState.NOT_RESOLVED;
readState = ReadState.IDLE;
super.dispose();
}

private void cancelScheduledTask() {
if (scheduledTask != null) {
scheduledTask.cancel(true);
scheduledTask = null;
}
}

private void executePeridioc() {
sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
execute();
}

private synchronized void execute() {
ConnectionState connectionState = device.getConnectionState();
logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
readState);

switch (connectionState) {
case DISCOVERING:
case DISCOVERED:
case DISCONNECTED:
if (isTimeToRead()) {
connect();
}
break;
case CONNECTED:
read();
break;
default:
break;
}
}

private void connect() {
logger.debug("Connect to device {}...", address);
if (!device.connect()) {
logger.debug("Connecting to device {} failed", address);
}
}

private void disconnect() {
logger.debug("Disconnect from device {}...", address);
if (!device.disconnect()) {
logger.debug("Disconnect from device {} failed", address);
}
}

private void read() {
switch (serviceState) {
case NOT_RESOLVED:
discoverServices();
break;
case RESOLVED:
switch (readState) {
case IDLE:
logger.debug("Read data from device {}...", address);
BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
if (characteristic != null && device.readCharacteristic(characteristic)) {
readState = ReadState.READING;
} else {
logger.debug("Read data from device {} failed", address);
disconnect();
}
break;
default:
break;
}
default:
break;
}
}

private void discoverServices() {
logger.debug("Discover services for device {}", address);
serviceState = ServiceState.RESOLVING;
device.discoverServices();
}

@Override
public void onServicesDiscovered() {
serviceState = ServiceState.RESOLVED;
logger.debug("Service discovery completed for device {}", address);
printServices();
execute();
}

private void printServices() {
device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
}

@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
switch (connectionNotification.getConnectionState()) {
case DISCONNECTED:
if (serviceState == ServiceState.RESOLVING) {
serviceState = ServiceState.NOT_RESOLVED;
}
readState = ReadState.IDLE;
break;
default:
break;

}
execute();
}

@Override
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
try {
if (status == BluetoothCompletionStatus.SUCCESS) {
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address,
characteristic.getValue());
updateStatus(ThingStatus.ONLINE);
sinceLastReadSec.set(0);
updateChannels(characteristic.getValue());
} else {
logger.debug("Characteristic {} from device {} failed", characteristic.getUuid(), address);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No response from device");
}
} finally {
readState = ReadState.IDLE;
disconnect();
}
}

private boolean isTimeToRead() {
int sinceLastRead = sinceLastReadSec.get();
logger.debug("Time since last update: {} sec", sinceLastRead);
return sinceLastRead >= refreshInterval;
}

/**
* Provides the UUID of the characteristic, which holds the sensor data
*
* @return the UUID of the data characteristic
*/
protected abstract UUID getDataUUID();

/**
* This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
*
* @param is the content of the bluetooth characteristic
*/
abstract protected void updateChannels(int[] is);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.binding.bluetooth.airthings.internal;

import java.math.BigInteger;
import java.util.Set;

import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
Expand All @@ -34,13 +35,19 @@
* used across the whole binding.
*
* @author Pauli Anttila - Initial contribution
* @author Kai Kreuzer - Added Airthings Wave Mini support
*/
@NonNullByDefault
public class AirthingsBindingConstants {

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_PLUS = new ThingTypeUID(
BluetoothBindingConstants.BINDING_ID, "airthings_wave_plus");
public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_MINI = new ThingTypeUID(
BluetoothBindingConstants.BINDING_ID, "airthings_wave_mini");

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIRTHINGS_WAVE_PLUS,
THING_TYPE_AIRTHINGS_WAVE_MINI);

// Channel IDs
public static final String CHANNEL_ID_HUMIDITY = "humidity";
Expand Down
Loading

0 comments on commit 6a4151a

Please sign in to comment.