From 0a6e5536e9e1fca8f8443b6095954b1e9f3c8ee1 Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Fri, 21 Jun 2024 19:25:28 +0200 Subject: [PATCH] =?UTF-8?q?[bluetooth.airthings]=C2=A0Add=20support=20for?= =?UTF-8?q?=20Airthings=20Wave=20Radon=20(#16879)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Wave Radon Signed-off-by: Arne Seime --- .../README.md | 13 +-- .../internal/AirthingsBindingConstants.java | 5 +- .../internal/AirthingsDataParser.java | 14 ++++ .../AirthingsDiscoveryParticipant.java | 9 +++ .../internal/AirthingsHandlerFactory.java | 4 + .../internal/AirthingsWaveRadonHandler.java | 79 +++++++++++++++++++ .../OH-INF/i18n/bluetooth.properties | 6 ++ .../main/resources/OH-INF/thing/airthings.xml | 32 ++++++++ .../airthings/AirthingsParserTest.java | 15 ++++ 9 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveRadonHandler.java diff --git a/bundles/org.openhab.binding.bluetooth.airthings/README.md b/bundles/org.openhab.binding.bluetooth.airthings/README.md index 916c9a3c74a..97b326903e1 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/README.md +++ b/bundles/org.openhab.binding.bluetooth.airthings/README.md @@ -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 | | ------------------ | -------------------------------- | ------------------------------------------- | diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsBindingConstants.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsBindingConstants.java index 9f710ddef67..55a61592f9d 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsBindingConstants.java +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsBindingConstants.java @@ -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 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"; 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 index ce80dd475b7..94d67341018 100644 --- 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 @@ -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 parseWaveRadonData(int[] data) throws AirthingsParserException { + if (data.length == EXPECTED_DATA_LEN) { + final Map 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); } diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDiscoveryParticipant.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDiscoveryParticipant.java index b04464a1e73..244fc52de48 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsDiscoveryParticipant.java @@ -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; } diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsHandlerFactory.java b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsHandlerFactory.java index 6f562b7e252..6ec03fa3e83 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsHandlerFactory.java +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsHandlerFactory.java @@ -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 index 00000000000..03a63df2a9c --- /dev/null +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/java/org/openhab/binding/bluetooth/airthings/internal/AirthingsWaveRadonHandler.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/i18n/bluetooth.properties b/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/i18n/bluetooth.properties index 41bc1cd4eb3..ab7d1306c67 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/i18n/bluetooth.properties +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/i18n/bluetooth.properties @@ -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 diff --git a/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/thing/airthings.xml b/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/thing/airthings.xml index 35ffb0b1095..f90f38b13f7 100644 --- a/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/thing/airthings.xml +++ b/bundles/org.openhab.binding.bluetooth.airthings/src/main/resources/OH-INF/thing/airthings.xml @@ -101,6 +101,38 @@ + + + + + + + + + Indoor air quality monitor with radon detection + + + + + + + + + + + + + Bluetooth address in XX:XX:XX:XX:XX:XX format + + + + States how often a refresh shall occur in seconds. This could have impact to battery lifetime + 300 + + + + + Number:Dimensionless 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 index 67c84f98e5e..c6699962af9 100644 --- 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 @@ -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 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 };