mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-27 07:41:39 +01:00
[bluetooth.airthings] Add support for Airthings Wave Radon (#16879)
* Add support for Wave Radon Signed-off-by: Arne Seime <arne.seime@gmail.com>
This commit is contained in:
parent
422b72eef5
commit
0a6e5536e9
@ -7,10 +7,11 @@ This extension adds support for [Airthings](https://www.airthings.com) indoor ai
|
|||||||
Following thing types are supported by this extension:
|
Following thing types are supported by this extension:
|
||||||
|
|
||||||
| Thing Type ID | Description |
|
| Thing Type ID | Description |
|
||||||
| ------------------- | -------------------------------------- |
|
|----------------------|----------------------------------------|
|
||||||
| airthings_wave_plus | Airthings Wave Plus |
|
| airthings_wave_plus | Airthings Wave Plus |
|
||||||
| airthings_wave_mini | Airthings Wave Mini |
|
| airthings_wave_mini | Airthings Wave Mini |
|
||||||
| airthings_wave_gen1 | Airthings Wave 1st Gen (SN 2900xxxxxx) |
|
| airthings_wave_gen1 | Airthings Wave 1st Gen (SN 2900xxxxxx) |
|
||||||
|
| airthings_wave_radon | Airthings Wave Radon / Wave 2 |
|
||||||
|
|
||||||
## Discovery
|
## 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_st_avg | Number:RadiationSpecificActivity | The measured radon short term average level |
|
||||||
| radon_lt_avg | Number:RadiationSpecificActivity | The measured radon long 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 |
|
| Channel ID | Item Type | Description |
|
||||||
| ------------------ | -------------------------------- | ------------------------------------------- |
|
| ------------------ | -------------------------------- | ------------------------------------------- |
|
||||||
|
@ -25,6 +25,7 @@ import org.openhab.core.thing.ThingTypeUID;
|
|||||||
* @author Pauli Anttila - Initial contribution
|
* @author Pauli Anttila - Initial contribution
|
||||||
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
||||||
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
||||||
|
* @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class AirthingsBindingConstants {
|
public class AirthingsBindingConstants {
|
||||||
@ -36,9 +37,11 @@ public class AirthingsBindingConstants {
|
|||||||
BluetoothBindingConstants.BINDING_ID, "airthings_wave_mini");
|
BluetoothBindingConstants.BINDING_ID, "airthings_wave_mini");
|
||||||
public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_GEN1 = new ThingTypeUID(
|
public static final ThingTypeUID THING_TYPE_AIRTHINGS_WAVE_GEN1 = new ThingTypeUID(
|
||||||
BluetoothBindingConstants.BINDING_ID, "airthings_wave_gen1");
|
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,
|
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
|
// Channel IDs
|
||||||
public static final String CHANNEL_ID_HUMIDITY = "humidity";
|
public static final String CHANNEL_ID_HUMIDITY = "humidity";
|
||||||
|
@ -24,6 +24,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
|||||||
*
|
*
|
||||||
* @author Pauli Anttila - Initial contribution
|
* @author Pauli Anttila - Initial contribution
|
||||||
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
||||||
|
* @author Arne Seime - Added Airthings Radon / Wave 2 support
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class AirthingsDataParser {
|
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) {
|
private static int intFromBytes(int lowByte, int highByte) {
|
||||||
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
|
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import org.osgi.service.component.annotations.Component;
|
|||||||
* @author Pauli Anttila - Initial contribution
|
* @author Pauli Anttila - Initial contribution
|
||||||
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
||||||
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
||||||
|
* @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@ -44,6 +45,7 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
|
|||||||
|
|
||||||
private static final String WAVE_PLUS_MODEL = "2930";
|
private static final String WAVE_PLUS_MODEL = "2930";
|
||||||
private static final String WAVE_MINI_MODEL = "2920";
|
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
|
private static final String WAVE_GEN1_MODEL = "2900"; // Wave 1st Gen SN 2900xxxxxx
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -66,6 +68,10 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
|
|||||||
return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_GEN1,
|
return new ThingUID(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_GEN1,
|
||||||
device.getAdapter().getUID(), device.getAddress().toString().toLowerCase().replace(":", ""));
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@ -88,6 +94,9 @@ public class AirthingsDiscoveryParticipant implements BluetoothDiscoveryParticip
|
|||||||
if (WAVE_GEN1_MODEL.equals(device.getModel())) {
|
if (WAVE_GEN1_MODEL.equals(device.getModel())) {
|
||||||
return createResult(device, thingUID, "Airthings Wave Gen 1");
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import org.osgi.service.component.annotations.Component;
|
|||||||
* @author Pauli Anttila - Initial contribution
|
* @author Pauli Anttila - Initial contribution
|
||||||
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
* @author Kai Kreuzer - Added Airthings Wave Mini support
|
||||||
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
* @author Davy Wong - Added Airthings Wave Gen 1 support
|
||||||
|
* @author Arne Seime - Added Airthings Wave Radon / Wave 2 support
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.airthings")
|
@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)) {
|
if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_GEN1)) {
|
||||||
return new AirthingsWaveGen1Handler(thing);
|
return new AirthingsWaveGen1Handler(thing);
|
||||||
}
|
}
|
||||||
|
if (thingTypeUID.equals(AirthingsBindingConstants.THING_TYPE_AIRTHINGS_WAVE_RADON)) {
|
||||||
|
return new AirthingsWaveRadonHandler(thing);
|
||||||
|
}
|
||||||
return null;
|
return 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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_mini.description = Indoor air quality monitor
|
||||||
thing-type.bluetooth.airthings_wave_plus.label = Airthings Wave Plus
|
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_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 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.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.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_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
|
# channel types
|
||||||
|
|
||||||
|
@ -101,6 +101,38 @@
|
|||||||
</config-description>
|
</config-description>
|
||||||
</thing-type>
|
</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">
|
<channel-type id="airthings_humidity">
|
||||||
<item-type unitHint="%">Number:Dimensionless</item-type>
|
<item-type unitHint="%">Number:Dimensionless</item-type>
|
||||||
<label>Humidity</label>
|
<label>Humidity</label>
|
||||||
|
@ -14,10 +14,12 @@ package org.openhab.binding.bluetooth.airthings;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.HexFormat;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.Test;
|
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.AirthingsDataParser;
|
||||||
import org.openhab.binding.bluetooth.airthings.internal.AirthingsParserException;
|
import org.openhab.binding.bluetooth.airthings.internal.AirthingsParserException;
|
||||||
|
|
||||||
@ -61,6 +63,19 @@ public class AirthingsParserTest {
|
|||||||
assertEquals(122, result.get(AirthingsDataParser.RADON_SHORT_TERM_AVG));
|
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
|
@Test
|
||||||
public void testParsingMini() throws AirthingsParserException {
|
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 };
|
int[] data = { 12, 0, 248, 112, 201, 193, 136, 14, 150, 0, 1, 0, 217, 176, 14, 0, 255, 255, 255, 255 };
|
||||||
|
Loading…
Reference in New Issue
Block a user