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
| 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 |
| ------------------ | -------------------------------- | ------------------------------------------- |
* @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 {
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";
*
* @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 {
}
}
+ 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);
}
* @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
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
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;
}
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;
}
* @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")
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;
}
}
--- /dev/null
+/**
+ * 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;
+ }
+}
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
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
</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>
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;
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 };