]> git.basschouten.com Git - openhab-addons.git/commitdiff
[bluetooth.airthings] Add support for Airthings Wave Radon (#16879)
authorArne Seime <seime@users.noreply.github.com>
Fri, 21 Jun 2024 17:25:28 +0000 (19:25 +0200)
committerGitHub <noreply@github.com>
Fri, 21 Jun 2024 17:25:28 +0000 (19:25 +0200)
* Add support for Wave Radon

Signed-off-by: Arne Seime <arne.seime@gmail.com>
bundles/org.openhab.binding.bluetooth.airthings/README.md
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
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/AirthingsWaveRadonHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/i18n/bluetooth.properties
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

index 916c9a3c74af06e1aa1cf8682f5b0fed5cf3e612..97b326903e101674c2f636bf973126c999d1343d 100644 (file)
@@ -6,11 +6,12 @@ This extension adds support for [Airthings](https://www.airthings.com) indoor ai
 
 Following thing types are supported by this extension:
 
-| Thing Type ID       | Description                            |
-| ------------------- | -------------------------------------- |
-| airthings_wave_plus | Airthings Wave Plus                    |
-| airthings_wave_mini | Airthings Wave Mini                    |
-| airthings_wave_gen1 | Airthings Wave 1st Gen (SN 2900xxxxxx) |
+| Thing Type ID        | Description                            |
+|----------------------|----------------------------------------|
+| airthings_wave_plus  | Airthings Wave Plus                    |
+| airthings_wave_mini  | Airthings Wave Mini                    |
+| airthings_wave_gen1  | Airthings Wave 1st Gen (SN 2900xxxxxx) |
+| airthings_wave_radon | Airthings Wave Radon / Wave 2          |
 
 ## Discovery
 
@@ -44,7 +45,7 @@ The `Airthings Wave Plus` thing has additionally the following channels:
 | radon_st_avg       | Number:RadiationSpecificActivity | The measured radon short term average level |
 | radon_lt_avg       | Number:RadiationSpecificActivity | The measured radon long term average level  |
 
-The `Airthings Wave Gen 1` thing has the following channels:
+The `Airthings Wave Gen 1` and `Airthings Wave Radon / Wave 2` thing has the following channels:
 
 | Channel ID         | Item Type                        | Description                                 |
 | ------------------ | -------------------------------- | ------------------------------------------- |
index 9f710ddef672efdafc3efbf6f04c77d7c8e3ddbc..55a61592f9de54539953db7e9e347ad71d212235 100644 (file)
@@ -25,6 +25,7 @@ import org.openhab.core.thing.ThingTypeUID;
  * @author Pauli Anttila - Initial contribution
  * @author Kai Kreuzer - Added Airthings Wave Mini support
  * @author Davy Wong - Added Airthings Wave Gen 1 support
+ * @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
  */
 @NonNullByDefault
 public class AirthingsBindingConstants {
@@ -36,9 +37,11 @@ public class AirthingsBindingConstants {
             BluetoothBindingConstants.BINDING_ID, "airthings_wave_mini");
     public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_GEN1 = new ThingTypeUID(
             BluetoothBindingConstants.BINDING_ID, "airthings_wave_gen1");
+    public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_RADON = new ThingTypeUID(
+            BluetoothBindingConstants.BINDING_ID, "airthings_wave_radon");
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_AIRTHINGS_WAVE_PLUS,
-            THING_TYPE_AIRTHINGS_WAVE_MINI, THING_TYPE_AIRTHINGS_WAVE_GEN1);
+            THING_TYPE_AIRTHINGS_WAVE_MINI, THING_TYPE_AIRTHINGS_WAVE_GEN1, THING_TYPE_AIRTHINGS_WAVE_RADON);
 
     // Channel IDs
     public static final String CHANNEL_ID_HUMIDITY = "humidity";
index ce80dd475b74ee7fe8f7cafd6a656836d5543c10..94d67341018a46e6793238308f573655df7d7df1 100644 (file)
@@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
  *
  * @author Pauli Anttila - Initial contribution
  * @author Kai Kreuzer - Added Airthings Wave Mini support
+ * @author Arne Seime - Added Airthings Radon / Wave 2 support
  */
 @NonNullByDefault
 public class AirthingsDataParser {
@@ -79,6 +80,19 @@ public class AirthingsDataParser {
         }
     }
 
+    public static Map<String, Number> parseWaveRadonData(int[] data) throws AirthingsParserException {
+        if (data.length == EXPECTED_DATA_LEN) {
+            final Map<String, Number> result = new HashMap<>();
+            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);
+            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 b04464a1e73b1b5ad93212f142bc72fed4eb8c33..244fc52de483f4ff1f27437cfe52ca3444cd0cd1 100644 (file)
@@ -34,6 +34,7 @@ import org.osgi.service.component.annotations.Component;
  * @author Pauli Anttila - Initial contribution
  * @author Kai Kreuzer - Added Airthings Wave Mini support
  * @author Davy Wong - Added Airthings Wave Gen 1 support
+ * @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
  *
  */
 @NonNullByDefault
@@ -44,6 +45,7 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
 
     private static final String WAVE_PLUS_MODEL = "2930";
     private static final String WAVE_MINI_MODEL = "2920";
+    private static final String WAVE_RADON_MODEL = "2950";
     private static final String WAVE_GEN1_MODEL = "2900"; // Wave 1st Gen SN 2900xxxxxx
 
     @Override
@@ -66,6 +68,10 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
                 return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_GEN1,
                         device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
             }
+            if (WAVE_RADON_MODEL.equals(device.getModel())) {
+                return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_RADON,
+                        device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
+            }
         }
         return null;
     }
@@ -88,6 +94,9 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
         if (WAVE_GEN1_MODEL.equals(device.getModel())) {
             return createResult(device, thingUID, "Airthings Wave Gen 1");
         }
+        if (WAVE_RADON_MODEL.equals(device.getModel())) {
+            return createResult(device, thingUID, "Airthings Radon / Wave 2");
+        }
         return null;
     }
 
index 6f562b7e2529bd245c42c3c3e754d7f7835f8282..6ec03fa3e830e039f0dc4fa127807aa30a924e54 100644 (file)
@@ -27,6 +27,7 @@ import org.osgi.service.component.annotations.Component;
  * @author Pauli Anttila - Initial contribution
  * @author Kai Kreuzer - Added Airthings Wave Mini support
  * @author Davy Wong - Added Airthings Wave Gen 1 support
+ * @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
  */
 @NonNullByDefault
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.airthings")
@@ -49,6 +50,9 @@ public class AirthingsHandlerFactory extends BaseThingHandlerFactory {
         if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_GEN1)) {
             return new AirthingsWaveGen1Handler(thing);
         }
+        if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_RADON)) {
+            return new AirthingsWaveRadonHandler(thing);
+        }
         return null;
     }
 }
diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveRadonHandler.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveRadonHandler.java
new file mode 100644 (file)
index 0000000..03a63df
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2024 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 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 AirthingsWaveRadonHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Arne Seime - Initial contribution
+ */
+@NonNullByDefault
+public class AirthingsWaveRadonHandler extends AbstractAirthingsHandler {
+
+    private static final String DATA_UUID = "b42e4dcc-ade7-11e4-89d3-123b93f75cba";
+
+    public AirthingsWaveRadonHandler(Thing thing) {
+        super(thing);
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(AirthingsWaveRadonHandler.class);
+    private final UUID uuid = UUID.fromString(DATA_UUID);
+
+    @Override
+    protected void updateChannels(int[] is) {
+        Map<String, Number> data;
+        try {
+            data = AirthingsDataParser.parseWaveRadonData(is);
+            logger.debug("Parsed data: {}", data);
+            Number humidity = data.get(AirthingsDataParser.HUMIDITY);
+            if (humidity != null) {
+                updateState(CHANNEL_ID_HUMIDITY, new QuantityType<>(humidity, Units.PERCENT));
+            }
+            Number temperature = data.get(AirthingsDataParser.TEMPERATURE);
+            if (temperature != null) {
+                updateState(CHANNEL_ID_TEMPERATURE, new QuantityType<>(temperature, SIUnits.CELSIUS));
+            }
+            Number radonShortTermAvg = data.get(AirthingsDataParser.RADON_SHORT_TERM_AVG);
+            if (radonShortTermAvg != null) {
+                updateState(CHANNEL_ID_RADON_ST_AVG,
+                        new QuantityType<>(radonShortTermAvg, Units.BECQUEREL_PER_CUBIC_METRE));
+            }
+            Number radonLongTermAvg = data.get(AirthingsDataParser.RADON_LONG_TERM_AVG);
+            if (radonLongTermAvg != null) {
+                updateState(CHANNEL_ID_RADON_LT_AVG,
+                        new QuantityType<>(radonLongTermAvg, Units.BECQUEREL_PER_CUBIC_METRE));
+            }
+        } catch (AirthingsParserException e) {
+            logger.warn("Failed to parse data received from Airthings sensor: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    protected UUID getDataUUID() {
+        return uuid;
+    }
+}
index 41bc1cd4eb3a870a12dc65082152a3d5e563f912..ab7d1306c6721c1848d463e0a9ee499a2916ce23 100644 (file)
@@ -6,6 +6,8 @@ thing-type.bluetooth.airthings_wave_mini.label = Airthings Wave Mini
 thing-type.bluetooth.airthings_wave_mini.description = Indoor air quality monitor
 thing-type.bluetooth.airthings_wave_plus.label = Airthings Wave Plus
 thing-type.bluetooth.airthings_wave_plus.description = Indoor air quality monitor with radon detection
+thing-type.bluetooth.airthings_wave_radon.label = Airthings Wave Radon / Wave 2
+thing-type.bluetooth.airthings_wave_radon.description = Smart Radon Monitor
 
 # thing types config
 
@@ -21,6 +23,10 @@ thing-type.config.bluetooth.airthings_wave_plus.address.label = Address
 thing-type.config.bluetooth.airthings_wave_plus.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
 thing-type.config.bluetooth.airthings_wave_plus.refreshInterval.label = Refresh Interval
 thing-type.config.bluetooth.airthings_wave_plus.refreshInterval.description = States how often a refresh shall occur in seconds. This could have impact to battery lifetime
+thing-type.config.bluetooth.airthings_wave_radon.address.label = Address
+thing-type.config.bluetooth.airthings_wave_radon.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
+thing-type.config.bluetooth.airthings_wave_radon.refreshInterval.label = Refresh Interval
+thing-type.config.bluetooth.airthings_wave_radon.refreshInterval.description = States how often a refresh shall occur in seconds. This could have impact to battery lifetime
 
 # channel types
 
index 35ffb0b1095bde6605509c9bf3021254da6e915d..f90f38b13f73ec4a0f0dfcffdce45e5e5fb8163c 100644 (file)
                </config-description>
        </thing-type>
 
+       <thing-type id="airthings_wave_radon">
+               <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 Radon / Wave 2</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="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 unitHint="%">Number:Dimensionless</item-type>
                <label>Humidity</label>
index 67c84f98e5eb59f9686f1a172b3741d0a1271d8f..c6699962af954bd0f5d8d90dfb9ba63fd98734e9 100644 (file)
@@ -14,10 +14,12 @@ package org.openhab.binding.bluetooth.airthings;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import java.util.HexFormat;
 import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.Test;
+import org.openhab.binding.bluetooth.BluetoothUtils;
 import org.openhab.binding.bluetooth.airthings.internal.AirthingsDataParser;
 import org.openhab.binding.bluetooth.airthings.internal.AirthingsParserException;
 
@@ -61,6 +63,19 @@ public class AirthingsParserTest {
         assertEquals(122, result.get(AirthingsDataParser.RADON_SHORT_TERM_AVG));
     }
 
+    @Test
+    public void testParsingWaveRadon() throws AirthingsParserException {
+        // Testdata from
+        // https://github.com/Airthings/airthings-ble/blob/9d255808fa3add6d504649e40c8548ffcd356909/tests/test_wave_plus.py#L47
+        byte[] data = HexFormat.of().parseHex("013860f009001100a709ffffffffffff0000ffff");
+        Map<String, Number> result = AirthingsDataParser.parseWaveRadonData(BluetoothUtils.toIntArray(data));
+
+        assertEquals(28.0, result.get(AirthingsDataParser.HUMIDITY));
+        assertEquals(24.71, result.get(AirthingsDataParser.TEMPERATURE));
+        assertEquals(17, result.get(AirthingsDataParser.RADON_LONG_TERM_AVG));
+        assertEquals(9, 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 };