added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.bluetooth.airthings-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-bluetooth-airthings" description="Bluetooth Binding Airthings" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-serial</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.airthings/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
</features>
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.math.BigInteger;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.library.dimension.Density;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
import tec.uom.se.format.SimpleUnitFormat;
|
||||
import tec.uom.se.function.RationalConverter;
|
||||
import tec.uom.se.unit.ProductUnit;
|
||||
import tec.uom.se.unit.TransformedUnit;
|
||||
import tec.uom.se.unit.Units;
|
||||
|
||||
/**
|
||||
* The {@link AirthingsBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@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");
|
||||
|
||||
// Channel IDs
|
||||
public static final String CHANNEL_ID_HUMIDITY = "humidity";
|
||||
public static final String CHANNEL_ID_TEMPERATURE = "temperature";
|
||||
public static final String CHANNEL_ID_PRESSURE = "pressure";
|
||||
public static final String CHANNEL_ID_CO2 = "co2";
|
||||
public static final String CHANNEL_ID_TVOC = "tvoc";
|
||||
public static final String CHANNEL_ID_RADON_ST_AVG = "radon_st_avg";
|
||||
public static final String CHANNEL_ID_RADON_LT_AVG = "radon_lt_avg";
|
||||
|
||||
public static final Unit<Dimensionless> PARTS_PER_BILLION = new TransformedUnit<>(SmartHomeUnits.ONE,
|
||||
new RationalConverter(BigInteger.ONE, BigInteger.valueOf(1000000000)));
|
||||
public static final Unit<Density> BECQUEREL_PER_CUBIC_METRE = new ProductUnit<>(
|
||||
Units.BECQUEREL.divide(Units.CUBIC_METRE));
|
||||
|
||||
static {
|
||||
SimpleUnitFormat.getInstance().label(PARTS_PER_BILLION, "ppb");
|
||||
SimpleUnitFormat.getInstance().label(BECQUEREL_PER_CUBIC_METRE, "Bq/m³");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Configuration class for {@link AirthingsBinding} device.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirthingsConfiguration {
|
||||
public String address = "";
|
||||
public int refreshInterval;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[address=" + address + ", refreshInterval=" + refreshInterval + "]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* This discovery participant is able to recognize Airthings devices and create discovery results for them.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true)
|
||||
public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
||||
|
||||
private static final int AIRTHINGS_COMPANY_ID = 820; // Formerly Corentium AS
|
||||
|
||||
private static final String WAVE_PLUS_MODEL = "2930";
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Collections.singleton(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
||||
if (isAirthingsDevice(device)) {
|
||||
if (WAVE_PLUS_MODEL.equals(device.getModel())) {
|
||||
return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS,
|
||||
device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
|
||||
if (!isAirthingsDevice(device)) {
|
||||
return null;
|
||||
}
|
||||
ThingUID thingUID = getThingUID(device);
|
||||
if (thingUID == null) {
|
||||
return null;
|
||||
}
|
||||
if (WAVE_PLUS_MODEL.equals(device.getModel())) {
|
||||
return createWavePlus(device, thingUID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresConnection(BluetoothDiscoveryDevice device) {
|
||||
return isAirthingsDevice(device);
|
||||
}
|
||||
|
||||
private boolean isAirthingsDevice(BluetoothDiscoveryDevice device) {
|
||||
Integer manufacturerId = device.getManufacturerId();
|
||||
if (manufacturerId != null && manufacturerId == AIRTHINGS_COMPANY_ID) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private DiscoveryResult createWavePlus(BluetoothDiscoveryDevice device, ThingUID thingUID) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
|
||||
properties.put(Thing.PROPERTY_VENDOR, "Airthings AS");
|
||||
String serialNumber = device.getSerialNumber();
|
||||
String firmwareRevision = device.getFirmwareRevision();
|
||||
String model = device.getModel();
|
||||
String hardwareRevision = device.getHardwareRevision();
|
||||
Integer txPower = device.getTxPower();
|
||||
if (serialNumber != null) {
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
|
||||
}
|
||||
if (firmwareRevision != null) {
|
||||
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareRevision);
|
||||
}
|
||||
if (model != null) {
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model);
|
||||
}
|
||||
if (hardwareRevision != null) {
|
||||
properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwareRevision);
|
||||
}
|
||||
if (txPower != null) {
|
||||
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
|
||||
}
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getAddress().toString());
|
||||
|
||||
// Create the discovery result and add to the inbox
|
||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
|
||||
.withBridge(device.getAdapter().getUID()).withLabel("Airthings Wave+").build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link AirthingsHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.airthings")
|
||||
public class AirthingsHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.singleton(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS)) {
|
||||
return new AirthingsWavePlusHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception for data parsing errors.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirthingsParserException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1;
|
||||
|
||||
public AirthingsParserException() {
|
||||
}
|
||||
|
||||
public AirthingsParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AirthingsParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AirthingsParserException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link AirthingsWavePlusDataParser} is responsible for parsing data from Wave Plus device format.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirthingsWavePlusDataParser {
|
||||
private static final int EXPECTED_DATA_LEN = 20;
|
||||
private static final int EXPECTED_VER = 1;
|
||||
|
||||
private double humidity;
|
||||
private int radonShortTermAvg;
|
||||
private int radonLongTermAvg;
|
||||
private double temperature;
|
||||
private double pressure;
|
||||
private int co2;
|
||||
private int tvoc;
|
||||
|
||||
public AirthingsWavePlusDataParser(int[] data) throws AirthingsParserException {
|
||||
parseData(data);
|
||||
}
|
||||
|
||||
public double getHumidity() {
|
||||
return humidity;
|
||||
}
|
||||
|
||||
public int getRadonShortTermAvg() {
|
||||
return radonShortTermAvg;
|
||||
}
|
||||
|
||||
public int getRadonLongTermAvg() {
|
||||
return radonLongTermAvg;
|
||||
}
|
||||
|
||||
public double getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public double getPressure() {
|
||||
return pressure;
|
||||
}
|
||||
|
||||
public int getCo2() {
|
||||
return co2;
|
||||
}
|
||||
|
||||
public int getTvoc() {
|
||||
return tvoc;
|
||||
}
|
||||
|
||||
private void parseData(int[] data) throws AirthingsParserException {
|
||||
if (data.length == EXPECTED_DATA_LEN) {
|
||||
final int version = data[0];
|
||||
|
||||
if (version == EXPECTED_VER) {
|
||||
humidity = data[1] / 2D;
|
||||
radonShortTermAvg = intFromBytes(data[4], data[5]);
|
||||
radonLongTermAvg = intFromBytes(data[6], data[7]);
|
||||
temperature = intFromBytes(data[8], data[9]) / 100D;
|
||||
pressure = intFromBytes(data[10], data[11]) / 50D;
|
||||
co2 = intFromBytes(data[12], data[13]);
|
||||
tvoc = intFromBytes(data[14], data[15]);
|
||||
} else {
|
||||
throw new AirthingsParserException(String.format("Unsupported data structure version '%d'", version));
|
||||
}
|
||||
} else {
|
||||
throw new AirthingsParserException(String.format("Illegal data structure length '%d'", data.length));
|
||||
}
|
||||
}
|
||||
|
||||
private int intFromBytes(int lowByte, int highByte) {
|
||||
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"[humidity=%.1f %%rH, radonShortTermAvg=%d Bq/m3, radonLongTermAvg=%d Bq/m3, temperature=%.1f °C, air pressure=%.2f mbar, co2=%d ppm, tvoc=%d ppb]",
|
||||
humidity, radonShortTermAvg, radonLongTermAvg, temperature, pressure, co2, tvoc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 static org.openhab.binding.bluetooth.airthings.internal.AirthingsBindingConstants.*;
|
||||
|
||||
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.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
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 AirthingsWavePlusHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private static final String DATA_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
|
||||
private static final int CHECK_PERIOD_SEC = 10;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AirthingsWavePlusHandler.class);
|
||||
private final UUID uuid = UUID.fromString(DATA_UUID);
|
||||
|
||||
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 AirthingsWavePlusHandler(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 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(uuid);
|
||||
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);
|
||||
try {
|
||||
updateChannels(new AirthingsWavePlusDataParser(characteristic.getValue()));
|
||||
} catch (AirthingsParserException e) {
|
||||
logger.warn("Data parsing error occured, when parsing data from device {}, cause {}", address,
|
||||
e.getMessage(), e);
|
||||
}
|
||||
} 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 void updateChannels(AirthingsWavePlusDataParser parser) {
|
||||
logger.debug("Parsed data: {}", parser);
|
||||
updateState(CHANNEL_ID_HUMIDITY,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getHumidity()), SmartHomeUnits.PERCENT));
|
||||
updateState(CHANNEL_ID_TEMPERATURE,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getTemperature()), SIUnits.CELSIUS));
|
||||
updateState(CHANNEL_ID_PRESSURE,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getPressure()), SmartHomeUnits.MILLIBAR));
|
||||
updateState(CHANNEL_ID_CO2,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getCo2()), SmartHomeUnits.PARTS_PER_MILLION));
|
||||
updateState(CHANNEL_ID_TVOC, QuantityType.valueOf(Double.valueOf(parser.getTvoc()), PARTS_PER_BILLION));
|
||||
updateState(CHANNEL_ID_RADON_ST_AVG,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getRadonShortTermAvg()), BECQUEREL_PER_CUBIC_METRE));
|
||||
updateState(CHANNEL_ID_RADON_LT_AVG,
|
||||
QuantityType.valueOf(Double.valueOf(parser.getRadonLongTermAvg()), BECQUEREL_PER_CUBIC_METRE));
|
||||
}
|
||||
|
||||
private boolean isTimeToRead() {
|
||||
int sinceLastRead = sinceLastReadSec.get();
|
||||
logger.debug("Time since last update: {} sec", sinceLastRead);
|
||||
return sinceLastRead >= refreshInterval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="bluetooth"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="airthings_wave_plus">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="roaming"/>
|
||||
<bridge-type-ref id="bluegiga"/>
|
||||
<bridge-type-ref id="bluez"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Airthings Wave+</label>
|
||||
<description>Indoor air quality monitor with radon detection</description>
|
||||
|
||||
<channels>
|
||||
<channel id="rssi" typeId="rssi"/>
|
||||
|
||||
<channel id="humidity" typeId="airthings_humidity"/>
|
||||
<channel id="temperature" typeId="airthings_temperature"/>
|
||||
<channel id="pressure" typeId="airthings_pressure"/>
|
||||
<channel id="co2" typeId="airthings_co2"/>
|
||||
<channel id="tvoc" typeId="airthings_tvoc"/>
|
||||
<channel id="radon_st_avg" typeId="airthings_radon_st_avg"/>
|
||||
<channel id="radon_lt_avg" typeId="airthings_radon_lt_avg"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="address" type="text">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="10">
|
||||
<label>Refresh Interval</label>
|
||||
<description>States how often a refresh shall occur in seconds. This could have impact to battery lifetime</description>
|
||||
<default>300</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="airthings_humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Humidity level</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Temperature</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_pressure">
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Pressure</label>
|
||||
<description>Pressure</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_co2">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>CO₂ Level</label>
|
||||
<description>Carbon dioxide level</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_tvoc">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>TVOC Level</label>
|
||||
<description>Total volatile organic compounds</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_radon_st_avg">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>Radon Short Term Average Level</label>
|
||||
<description>Radon gas level</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="airthings_radon_lt_avg">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>Radon Long Term Average Level</label>
|
||||
<description>Radon gas level</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.bluetooth.airthings.internal.AirthingsParserException;
|
||||
import org.openhab.binding.bluetooth.airthings.internal.AirthingsWavePlusDataParser;
|
||||
|
||||
/**
|
||||
* Tests {@link AirthingsWavePlusParserTest}.
|
||||
*
|
||||
* @author Pauli Anttila - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AirthingsWavePlusParserTest {
|
||||
|
||||
@Test(expected = AirthingsParserException.class)
|
||||
public void testWrongVersion() throws AirthingsParserException {
|
||||
int[] data = { 5, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0, 0, 4, 20 };
|
||||
new AirthingsWavePlusDataParser(data);
|
||||
}
|
||||
|
||||
@Test(expected = AirthingsParserException.class)
|
||||
public void testEmptyData() throws AirthingsParserException {
|
||||
int[] data = {};
|
||||
new AirthingsWavePlusDataParser(data);
|
||||
}
|
||||
|
||||
@Test(expected = AirthingsParserException.class)
|
||||
public void testWrongDataLen() throws AirthingsParserException {
|
||||
int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0 };
|
||||
new AirthingsWavePlusDataParser(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParsing() throws AirthingsParserException {
|
||||
int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0, 0, 4, 20 };
|
||||
AirthingsWavePlusDataParser parser = new AirthingsWavePlusDataParser(data);
|
||||
|
||||
assertEquals(27.5, parser.getHumidity(), 0.01);
|
||||
assertEquals(681, parser.getCo2());
|
||||
assertEquals(46, parser.getTvoc());
|
||||
assertEquals(24.23, parser.getTemperature(), 0.01);
|
||||
assertEquals(993.5, parser.getPressure(), 0.01);
|
||||
assertEquals(61, parser.getRadonLongTermAvg());
|
||||
assertEquals(122, parser.getRadonShortTermAvg());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user