]> git.basschouten.com Git - openhab-addons.git/commitdiff
[airthings] Add support for Airthings Wave Mini (#10456)
authorKai Kreuzer <kai@openhab.org>
Fri, 16 Apr 2021 19:38:20 +0000 (21:38 +0200)
committerGitHub <noreply@github.com>
Fri, 16 Apr 2021 19:38:20 +0000 (21:38 +0200)
Signed-off-by: Kai Kreuzer <kai@openhab.org>
12 files changed:
bundles/org.openhab.binding.bluetooth.airthings/README.md
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AbstractAirthingsHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsBindingConstants.java
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDataParser.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDiscoveryParticipant.java
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsHandlerFactory.java
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveMiniHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusDataParser.java [deleted file]
bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusHandler.java
bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/thing/airthings.xml
bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsParserTest.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsWavePlusParserTest.java [deleted file]

index eecba58d1e9d8a752432b70ea8053cf5d549334f..fe97bbc46917f14717cd554938677b50340cedae 100644 (file)
@@ -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
@@ -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                                                     |
 |---------------------------------|---------|---------|----------|-----------------------------------------------------------------|
@@ -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`:
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AbstractAirthingsHandler.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AbstractAirthingsHandler.java
new file mode 100644 (file)
index 0000000..51b30d8
--- /dev/null
@@ -0,0 +1,233 @@
+/**
+ * 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.BluetoothDevice.ConnectionState;
+import org.openhab.binding.bluetooth.BluetoothUtils;
+import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+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) {
+                            readState = ReadState.READING;
+                            device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
+                                try {
+                                    logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
+                                            address, data);
+                                    updateStatus(ThingStatus.ONLINE);
+                                    sinceLastReadSec.set(0);
+                                    updateChannels(BluetoothUtils.toIntArray(data));
+                                } finally {
+                                    readState = ReadState.IDLE;
+                                    disconnect();
+                                }
+                            });
+                        } 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();
+    }
+
+    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);
+}
index fed6f775e844050ae364f2b061a4401d2a3cdde0..326c7a4c7d74a918f0da05b9bbac82b45f4c79ab 100644 (file)
@@ -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;
@@ -34,6 +35,7 @@ import tec.uom.se.unit.TransformedUnit;
  * used across the whole binding.
  *
  * @author Pauli Anttila - Initial contribution
+ * @author Kai Kreuzer - Added Airthings Wave Mini support
  */
 @NonNullByDefault
 public class AirthingsBindingConstants {
@@ -41,6 +43,11 @@ 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";
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDataParser.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDataParser.java
new file mode 100644 (file)
index 0000000..80efba9
--- /dev/null
@@ -0,0 +1,85 @@
+/**
+ * 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.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link AirthingsDataParser} is responsible for parsing data from Wave Plus device format.
+ *
+ * @author Pauli Anttila - Initial contribution
+ * @author Kai Kreuzer - Added Airthings Wave Mini support
+ */
+@NonNullByDefault
+public class AirthingsDataParser {
+    public static final String TVOC = "tvoc";
+    public static final String CO2 = "co2";
+    public static final String PRESSURE = "pressure";
+    public static final String TEMPERATURE = "temperature";
+    public static final String RADON_LONG_TERM_AVG = "radonLongTermAvg";
+    public static final String RADON_SHORT_TERM_AVG = "radonShortTermAvg";
+    public static final String HUMIDITY = "humidity";
+
+    private static final int EXPECTED_DATA_LEN = 20;
+    private static final int EXPECTED_VER_PLUS = 1;
+
+    private AirthingsDataParser() {
+    }
+
+    public static Map<String, Number> parseWavePlusData(int[] data) throws AirthingsParserException {
+        if (data.length == EXPECTED_DATA_LEN) {
+            final Map<String, Number> result = new HashMap<>();
+
+            final int version = data[0];
+
+            if (version == EXPECTED_VER_PLUS) {
+                result.put(HUMIDITY, data[1] / 2D);
+                result.put(RADON_SHORT_TERM_AVG, intFromBytes(data[4], data[5]));
+                result.put(RADON_LONG_TERM_AVG, intFromBytes(data[6], data[7]));
+                result.put(TEMPERATURE, intFromBytes(data[8], data[9]) / 100D);
+                result.put(PRESSURE, intFromBytes(data[10], data[11]) / 50D);
+                result.put(CO2, intFromBytes(data[12], data[13]));
+                result.put(TVOC, intFromBytes(data[14], data[15]));
+                return result;
+            } 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));
+        }
+    }
+
+    public static Map<String, Number> parseWaveMiniData(int[] data) throws AirthingsParserException {
+        if (data.length == EXPECTED_DATA_LEN) {
+            final Map<String, Number> result = new HashMap<>();
+            result.put(TEMPERATURE,
+                    new BigDecimal(intFromBytes(data[2], data[3]))
+                            .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)
+                            .subtract(BigDecimal.valueOf(273.15)).doubleValue());
+            result.put(HUMIDITY, intFromBytes(data[6], data[7]) / 100D);
+            result.put(TVOC, intFromBytes(data[8], data[9]));
+            return result;
+        } else {
+            throw new AirthingsParserException(String.format("Illegal data structure length '%d'", data.length));
+        }
+    }
+
+    private static int intFromBytes(int lowByte, int highByte) {
+        return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
+    }
+}
index 58fd3f3da43716862eb239812a99d786619e885a..91eb0ee9558c2e2b7a9f9d7da7760948d8c228dd 100644 (file)
@@ -12,7 +12,6 @@
  */
 package org.openhab.binding.bluetooth.airthings.internal;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -33,6 +32,7 @@ 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
+ * @author Kai Kreuzer - Added Airthings Wave Mini support
  *
  */
 @NonNullByDefault
@@ -42,10 +42,11 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
     private static final int AIRTHINGS_COMPANY_ID = 820; // Formerly Corentium AS
 
     private static final String WAVE_PLUS_MODEL = "2930";
+    private static final String WAVE_MINI_MODEL = "2920";
 
     @Override
     public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
-        return Collections.singleton(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS);
+        return AirthingsBindingConstants.SUPPORTED_THING_TYPES_UIDS;
     }
 
     @Override
@@ -55,6 +56,10 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
                 return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS,
                         device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
             }
+            if (WAVE_MINI_MODEL.equals(device.getModel())) {
+                return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_MINI,
+                        device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
+            }
         }
         return null;
     }
@@ -69,7 +74,10 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
             return null;
         }
         if (WAVE_PLUS_MODEL.equals(device.getModel())) {
-            return createWavePlus(device, thingUID);
+            return createResult(device, thingUID, "Airthings Wave Plus");
+        }
+        if (WAVE_MINI_MODEL.equals(device.getModel())) {
+            return createResult(device, thingUID, "Airthings Wave Mini");
         }
         return null;
     }
@@ -87,7 +95,7 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
         return false;
     }
 
-    private DiscoveryResult createWavePlus(BluetoothDiscoveryDevice device, ThingUID thingUID) {
+    private DiscoveryResult createResult(BluetoothDiscoveryDevice device, ThingUID thingUID, String label) {
         Map<String, Object> properties = new HashMap<>();
         properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
         properties.put(Thing.PROPERTY_VENDOR, "Airthings AS");
@@ -116,6 +124,6 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
         // 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();
+                .withBridge(device.getAdapter().getUID()).withLabel(label).build();
     }
 }
index f2df72429dece0021c1abf4330e2585151f3fd7e..1b1c2b5b0c42ba06c5b436a8bf563e1f66d65907 100644 (file)
@@ -12,9 +12,6 @@
  */
 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;
@@ -28,17 +25,15 @@ import org.osgi.service.component.annotations.Component;
  * The {@link AirthingsHandlerFactory} is responsible for creating things and thing handlers.
  *
  * @author Pauli Anttila - Initial contribution
+ * @author Kai Kreuzer - Added Airthings Wave Mini support
  */
 @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);
+        return AirthingsBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
     }
 
     @Override
@@ -47,6 +42,9 @@ public class AirthingsHandlerFactory extends BaseThingHandlerFactory {
         if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_PLUS)) {
             return new AirthingsWavePlusHandler(thing);
         }
+        if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_MINI)) {
+            return new AirthingsWaveMiniHandler(thing);
+        }
         return null;
     }
 }
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveMiniHandler.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveMiniHandler.java
new file mode 100644 (file)
index 0000000..206e043
--- /dev/null
@@ -0,0 +1,77 @@
+/**
+ * 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 static org.openhab.binding.bluetooth.airthings.internal.AirthingsBindingConstants.*;
+
+import java.util.Map;
+import java.util.UUID;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link AirthingsWaveMiniHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ */
+@NonNullByDefault
+public class AirthingsWaveMiniHandler extends AbstractAirthingsHandler {
+
+    private static final String DATA_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
+
+    public AirthingsWaveMiniHandler(Thing thing) {
+        super(thing);
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(AirthingsWaveMiniHandler.class);
+
+    private final UUID uuid = UUID.fromString(DATA_UUID);
+
+    @Override
+    protected void updateChannels(int[] is) {
+        Map<String, Number> data;
+        try {
+            data = AirthingsDataParser.parseWaveMiniData(is);
+            logger.debug("Parsed data: {}", data);
+            Number humidity = data.get(AirthingsDataParser.HUMIDITY);
+            if (humidity != null) {
+                updateState(CHANNEL_ID_HUMIDITY, new QuantityType<Dimensionless>(humidity, Units.PERCENT));
+            }
+            Number temperature = data.get(AirthingsDataParser.TEMPERATURE);
+            if (temperature != null) {
+                updateState(CHANNEL_ID_TEMPERATURE, new QuantityType<Temperature>(temperature, SIUnits.CELSIUS));
+            }
+            Number tvoc = data.get(AirthingsDataParser.TVOC);
+            if (tvoc != null) {
+                updateState(CHANNEL_ID_TVOC, new QuantityType<Dimensionless>(tvoc, PARTS_PER_BILLION));
+            }
+        } catch (AirthingsParserException e) {
+            logger.error("Failed to parse data received from Airthings sensor: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    protected UUID getDataUUID() {
+        return uuid;
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusDataParser.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWavePlusDataParser.java
deleted file mode 100644 (file)
index d2c4f8f..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * 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 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);
-    }
-}
index 4cac3551856c4d4b618cd8dda7fc063d76aaf91d..6b24d5061825019b46982746a404bd477b9f8762 100644 (file)
@@ -14,25 +14,19 @@ package org.openhab.binding.bluetooth.airthings.internal;
 
 import static org.openhab.binding.bluetooth.airthings.internal.AirthingsBindingConstants.*;
 
-import java.util.Optional;
+import java.util.Map;
 import java.util.UUID;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Pressure;
+import javax.measure.quantity.Temperature;
 
 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.BluetoothDevice.ConnectionState;
-import org.openhab.binding.bluetooth.BluetoothUtils;
-import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
+import org.openhab.core.library.dimension.Density;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.unit.SIUnits;
 import org.openhab.core.library.unit.Units;
 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;
 
@@ -41,214 +35,63 @@ import org.slf4j.LoggerFactory;
  * sent to one of the channels.
  *
  * @author Pauli Anttila - Initial contribution
+ * @author Kai Kreuzer - Added Airthings Wave Mini support
  */
 @NonNullByDefault
-public class AirthingsWavePlusHandler extends BeaconBluetoothHandler {
+public class AirthingsWavePlusHandler extends AbstractAirthingsHandler {
 
     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
-    }
+    private final Logger logger = LoggerFactory.getLogger(AirthingsWavePlusHandler.class);
+    private final UUID uuid = UUID.fromString(DATA_UUID);
 
     @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;
+    protected void updateChannels(int[] is) {
+        Map<String, Number> data;
+        try {
+            data = AirthingsDataParser.parseWavePlusData(is);
+            logger.debug("Parsed data: {}", data);
+            Number humidity = data.get(AirthingsDataParser.HUMIDITY);
+            if (humidity != null) {
+                updateState(CHANNEL_ID_HUMIDITY, new QuantityType<Dimensionless>(humidity, Units.PERCENT));
+            }
+            Number temperature = data.get(AirthingsDataParser.TEMPERATURE);
+            if (temperature != null) {
+                updateState(CHANNEL_ID_TEMPERATURE, new QuantityType<Temperature>(temperature, SIUnits.CELSIUS));
+            }
+            Number pressure = data.get(AirthingsDataParser.PRESSURE);
+            if (pressure != null) {
+                updateState(CHANNEL_ID_PRESSURE, new QuantityType<Pressure>(pressure, Units.MILLIBAR));
+            }
+            Number co2 = data.get(AirthingsDataParser.CO2);
+            if (co2 != null) {
+                updateState(CHANNEL_ID_CO2, new QuantityType<Dimensionless>(co2, Units.PARTS_PER_MILLION));
+            }
+            Number tvoc = data.get(AirthingsDataParser.TVOC);
+            if (tvoc != null) {
+                updateState(CHANNEL_ID_TVOC, new QuantityType<Dimensionless>(tvoc, PARTS_PER_BILLION));
+            }
+            Number radonShortTermAvg = data.get(AirthingsDataParser.RADON_SHORT_TERM_AVG);
+            if (radonShortTermAvg != null) {
+                updateState(CHANNEL_ID_RADON_ST_AVG,
+                        new QuantityType<Density>(radonShortTermAvg, BECQUEREL_PER_CUBIC_METRE));
+            }
+            Number radonLongTermAvg = data.get(AirthingsDataParser.RADON_LONG_TERM_AVG);
+            if (radonLongTermAvg != null) {
+                updateState(CHANNEL_ID_RADON_LT_AVG,
+                        new QuantityType<Density>(radonLongTermAvg, BECQUEREL_PER_CUBIC_METRE));
+            }
+        } catch (AirthingsParserException e) {
+            logger.error("Failed to parse data received from Airthings sensor: {}", e.getMessage());
         }
     }
 
-    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) {
-                            readState = ReadState.READING;
-                            device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
-                                try {
-                                    if (data != null) {
-                                        logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(),
-                                                address, data);
-                                        updateStatus(ThingStatus.ONLINE);
-                                        sinceLastReadSec.set(0);
-                                        try {
-                                            updateChannels(
-                                                    new AirthingsWavePlusDataParser(BluetoothUtils.toIntArray(data)));
-                                        } 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, ex.getMessage());
-                                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
-                                                ex.getMessage());
-                                    }
-                                } finally {
-                                    readState = ReadState.IDLE;
-                                    disconnect();
-                                }
-                            });
-                        } 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();
-    }
-
-    private void updateChannels(AirthingsWavePlusDataParser parser) {
-        logger.debug("Parsed data: {}", parser);
-        updateState(CHANNEL_ID_HUMIDITY, QuantityType.valueOf(Double.valueOf(parser.getHumidity()), Units.PERCENT));
-        updateState(CHANNEL_ID_TEMPERATURE,
-                QuantityType.valueOf(Double.valueOf(parser.getTemperature()), SIUnits.CELSIUS));
-        updateState(CHANNEL_ID_PRESSURE, QuantityType.valueOf(Double.valueOf(parser.getPressure()), Units.MILLIBAR));
-        updateState(CHANNEL_ID_CO2, QuantityType.valueOf(Double.valueOf(parser.getCo2()), Units.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;
+    protected UUID getDataUUID() {
+        return uuid;
     }
 }
index 0e3942057ff81feb6bbd8034a2fb79b70377a1da..3f7419ac5f0d4cb33bf3c1a3dea1b1272eea1389 100644 (file)
@@ -4,6 +4,37 @@
        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_mini">
+               <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 Mini</label>
+               <description>Indoor air quality monitor</description>
+
+               <channels>
+                       <channel id="rssi" typeId="rssi"/>
+
+                       <channel id="humidity" typeId="airthings_humidity"/>
+                       <channel id="temperature" typeId="airthings_temperature"/>
+                       <channel id="tvoc" typeId="airthings_tvoc"/>
+               </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>
+
        <thing-type id="airthings_wave_plus">
                <supported-bridge-type-refs>
                        <bridge-type-ref id="roaming"/>
@@ -11,7 +42,7 @@
                        <bridge-type-ref id="bluez"/>
                </supported-bridge-type-refs>
 
-               <label>Airthings Wave+</label>
+               <label>Airthings Wave Plus</label>
                <description>Indoor air quality monitor with radon detection</description>
 
                <channels>
@@ -43,7 +74,7 @@
                <item-type>Number:Dimensionless</item-type>
                <label>Humidity</label>
                <description>Humidity level</description>
-               <state readOnly="true" pattern="%.1f %unit%"/>
+               <state readOnly="true" pattern="%.1f %%"/>
        </channel-type>
        <channel-type id="airthings_temperature">
                <item-type>Number:Temperature</item-type>
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsParserTest.java b/bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsParserTest.java
new file mode 100644 (file)
index 0000000..d4662b6
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.bluetooth.airthings.internal.AirthingsDataParser;
+import org.openhab.binding.bluetooth.airthings.internal.AirthingsParserException;
+
+/**
+ * Tests {@link AirthingsParserTest}.
+ *
+ * @author Pauli Anttila - Initial contribution
+ */
+@NonNullByDefault
+public class AirthingsParserTest {
+
+    @Test
+    public void testWrongVersion() {
+        int[] data = { 5, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0, 0, 4, 20 };
+        assertThrows(AirthingsParserException.class, () -> AirthingsDataParser.parseWavePlusData(data));
+    }
+
+    @Test
+    public void testEmptyData() {
+        int[] data = {};
+        assertThrows(AirthingsParserException.class, () -> AirthingsDataParser.parseWavePlusData(data));
+    }
+
+    @Test
+    public void testWrongDataLen() throws AirthingsParserException {
+        int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0 };
+        assertThrows(AirthingsParserException.class, () -> AirthingsDataParser.parseWavePlusData(data));
+    }
+
+    @Test
+    public void testParsingPlus() throws AirthingsParserException {
+        int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0, 0, 4, 20 };
+        Map<String, Number> result = AirthingsDataParser.parseWavePlusData(data);
+
+        assertEquals(27.5, result.get(AirthingsDataParser.HUMIDITY));
+        assertEquals(681, result.get(AirthingsDataParser.CO2));
+        assertEquals(46, result.get(AirthingsDataParser.TVOC));
+        assertEquals(24.23, result.get(AirthingsDataParser.TEMPERATURE));
+        assertEquals(993.5, result.get(AirthingsDataParser.PRESSURE));
+        assertEquals(61, result.get(AirthingsDataParser.RADON_LONG_TERM_AVG));
+        assertEquals(122, result.get(AirthingsDataParser.RADON_SHORT_TERM_AVG));
+    }
+
+    @Test
+    public void testParsingMini() throws AirthingsParserException {
+        int[] data = { 12, 0, 248, 112, 201, 193, 136, 14, 150, 0, 1, 0, 217, 176, 14, 0, 255, 255, 255, 255 };
+        Map<String, Number> result = AirthingsDataParser.parseWaveMiniData(data);
+
+        assertEquals(37.2, result.get(AirthingsDataParser.HUMIDITY));
+        assertEquals(150, result.get(AirthingsDataParser.TVOC));
+        assertEquals(16.05, result.get(AirthingsDataParser.TEMPERATURE));
+    }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsWavePlusParserTest.java b/bundles/org.openhab.binding.bluetooth.airthings/src/test/java/org/openhab/binding/bluetooth/airthings/AirthingsWavePlusParserTest.java
deleted file mode 100644 (file)
index ba67de3..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * 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;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.junit.jupiter.api.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
-    public void testWrongVersion() {
-        int[] data = { 5, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0, 0, 4, 20 };
-        assertThrows(AirthingsParserException.class, () -> new AirthingsWavePlusDataParser(data));
-    }
-
-    @Test
-    public void testEmptyData() {
-        int[] data = {};
-        assertThrows(AirthingsParserException.class, () -> new AirthingsWavePlusDataParser(data));
-    }
-
-    @Test
-    public void testWrongDataLen() throws AirthingsParserException {
-        int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0 };
-        assertThrows(AirthingsParserException.class, () -> 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());
-    }
-}