mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-02-04 19:34:05 +01:00
[radoneye] Fixed the binding for HW v1 and v2 (#18125)
Signed-off-by: Jörg Sautter <joerg.sautter@gmx.net>
This commit is contained in:
parent
660acbf02e
commit
c79fdc5c4c
@ -28,9 +28,10 @@ Supported configuration parameters for the things:
|
|||||||
|
|
||||||
Following channels are supported for `RadonEye` thing:
|
Following channels are supported for `RadonEye` thing:
|
||||||
|
|
||||||
| Channel ID | Item Type | Description |
|
| Channel ID | Item Type | Description |
|
||||||
| ------------------ | ------------------------ | ------------------------------------------- |
|
|------------|----------------------------------|----------------------------------------|
|
||||||
| radon | Number:Density | The measured radon level |
|
| radon | Number:RadiationSpecificActivity | The measured radon level |
|
||||||
|
| decay | Number:Dimensionless | The decay count in the last time frame |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@ -43,5 +44,6 @@ bluetooth:radoneye_rd200:adapter1:sensor1 "radoneye Wave Plus Sensor 1" (blueto
|
|||||||
radoneye.items:
|
radoneye.items:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Number:Density radon "Radon level [%d %unit%]" { channel="bluetooth:radoneye_rd200:adapter1:sensor1:radon" }
|
Number:RadiationSpecificActivity radon "Radon level [%d %unit%]" { channel="bluetooth:radoneye_rd200:adapter1:sensor1:radon" }
|
||||||
|
Number:Dimensionless decay "Decay count [%d %unit%]" { channel="bluetooth:radoneye_rd200:adapter1:sensor1:decay" }
|
||||||
```
|
```
|
||||||
|
@ -15,18 +15,15 @@ package org.openhab.binding.bluetooth.radoneye.internal;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
|
||||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||||
import org.openhab.binding.bluetooth.BluetoothUtils;
|
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||||
import org.openhab.core.thing.Thing;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -35,39 +32,17 @@ import org.slf4j.LoggerFactory;
|
|||||||
* sent to one of the channels.
|
* sent to one of the channels.
|
||||||
*
|
*
|
||||||
* @author Peter Obel - Initial contribution
|
* @author Peter Obel - Initial contribution
|
||||||
|
* @author Jörg Sautter - Use the ConnectedBluetoothHandler the handle the connection state
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public abstract class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
|
public abstract class AbstractRadoneyeHandler extends ConnectedBluetoothHandler {
|
||||||
|
|
||||||
private static final int CHECK_PERIOD_SEC = 10;
|
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
|
||||||
|
private final AtomicLong isNotifying = new AtomicLong(-1);
|
||||||
|
|
||||||
private AtomicInteger sinceLastReadSec = new AtomicInteger();
|
|
||||||
private RadoneyeConfiguration configuration = new RadoneyeConfiguration();
|
private RadoneyeConfiguration configuration = new RadoneyeConfiguration();
|
||||||
private @Nullable ScheduledFuture<?> scheduledTask;
|
private @Nullable ScheduledFuture<?> scheduledTask;
|
||||||
|
|
||||||
private volatile int errorConnectCounter;
|
|
||||||
private volatile int errorReadCounter;
|
|
||||||
private volatile int errorWriteCounter;
|
|
||||||
private volatile int errorDisconnectCounter;
|
|
||||||
private volatile int errorResolvingCounter;
|
|
||||||
|
|
||||||
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,
|
|
||||||
WRITING
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractRadoneyeHandler(Thing thing) {
|
public AbstractRadoneyeHandler(Thing thing) {
|
||||||
super(thing);
|
super(thing);
|
||||||
}
|
}
|
||||||
@ -80,216 +55,81 @@ public abstract class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
|
|||||||
logger.debug("Using configuration: {}", configuration);
|
logger.debug("Using configuration: {}", configuration);
|
||||||
cancelScheduledTask();
|
cancelScheduledTask();
|
||||||
logger.debug("Start scheduled task to read device in every {} seconds", configuration.refreshInterval);
|
logger.debug("Start scheduled task to read device in every {} seconds", configuration.refreshInterval);
|
||||||
scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
|
scheduledTask = scheduler.scheduleWithFixedDelay(this::execute, configuration.refreshInterval,
|
||||||
TimeUnit.SECONDS);
|
configuration.refreshInterval, TimeUnit.SECONDS);
|
||||||
|
|
||||||
sinceLastReadSec.set(configuration.refreshInterval); // update immediately
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
logger.debug("Dispose");
|
logger.debug("Dispose");
|
||||||
cancelScheduledTask();
|
cancelScheduledTask();
|
||||||
serviceState = ServiceState.NOT_RESOLVED;
|
|
||||||
readState = ReadState.IDLE;
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelScheduledTask() {
|
private void cancelScheduledTask() {
|
||||||
if (scheduledTask != null) {
|
ScheduledFuture<?> task = scheduledTask;
|
||||||
scheduledTask.cancel(true);
|
if (task != null) {
|
||||||
|
task.cancel(false);
|
||||||
scheduledTask = null;
|
scheduledTask = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void executePeridioc() {
|
private void execute() {
|
||||||
sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
|
try {
|
||||||
execute();
|
long since = isNotifying.get();
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void execute() {
|
if (since != -1) {
|
||||||
ConnectionState connectionState = device.getConnectionState();
|
logger.debug("Send trigger data to device {}", address);
|
||||||
logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
|
writeCharacteristic(getServiceUUID(), getTriggerUUID(), getTriggerData(), false).exceptionally((t) -> {
|
||||||
readState);
|
String message = "Failed to send trigger data to device " + address + ", disconnect";
|
||||||
|
logger.warn(message, t);
|
||||||
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()) {
|
|
||||||
errorConnectCounter++;
|
|
||||||
if (errorConnectCounter < 6) {
|
|
||||||
logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
|
|
||||||
} else {
|
|
||||||
logger.debug("ERROR: Controller reset needed. Connecting to device {} failed {} times", address,
|
|
||||||
errorConnectCounter);
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.debug("Connected to device {}", address);
|
|
||||||
errorConnectCounter = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disconnect() {
|
|
||||||
logger.debug("Disconnect from device {}...", address);
|
|
||||||
if (!device.disconnect()) {
|
|
||||||
errorDisconnectCounter++;
|
|
||||||
if (errorDisconnectCounter < 6) {
|
|
||||||
logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
|
|
||||||
} else {
|
|
||||||
logger.debug("ERROR: Controller reset needed. Disconnect from device {} failed {} times", address,
|
|
||||||
errorDisconnectCounter);
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
|
||||||
"Disconnect from device failed");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.debug("Disconnected from device {}", address);
|
|
||||||
errorDisconnectCounter = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void read() {
|
|
||||||
switch (serviceState) {
|
|
||||||
case NOT_RESOLVED:
|
|
||||||
logger.debug("Discover services on device {}", address);
|
|
||||||
discoverServices();
|
|
||||||
break;
|
|
||||||
case RESOLVED:
|
|
||||||
switch (readState) {
|
|
||||||
case IDLE:
|
|
||||||
if (getTriggerUUID() != null) {
|
|
||||||
logger.debug("Send trigger data to device {}...", address);
|
|
||||||
BluetoothCharacteristic characteristic = device.getCharacteristic(getTriggerUUID());
|
|
||||||
if (characteristic != null) {
|
|
||||||
readState = ReadState.WRITING;
|
|
||||||
errorWriteCounter = 0;
|
|
||||||
device.writeCharacteristic(characteristic, getTriggerData()).whenComplete((v, ex) -> {
|
|
||||||
readSensorData();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
errorWriteCounter++;
|
|
||||||
if (errorWriteCounter < 6) {
|
|
||||||
logger.debug("Read/write data from device {} failed {} times", address,
|
|
||||||
errorWriteCounter);
|
|
||||||
} else {
|
|
||||||
logger.debug(
|
|
||||||
"ERROR: Controller reset needed. Read/write data from device {} failed {} times",
|
|
||||||
address, errorWriteCounter);
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
|
||||||
"Read/write data from device failed");
|
|
||||||
}
|
|
||||||
disconnect();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readSensorData();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default: // serviceState RESOLVING
|
|
||||||
errorResolvingCounter++;
|
|
||||||
if (errorResolvingCounter < 6) {
|
|
||||||
logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
|
|
||||||
} else {
|
|
||||||
logger.debug("ERROR: Controller reset needed. Unhandled serviceState {} on device {}",
|
|
||||||
serviceState, address);
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
|
||||||
"Service discovery for device failed");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readSensorData() {
|
|
||||||
logger.debug("Read data from device {}...", address);
|
|
||||||
BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
|
|
||||||
if (characteristic != null) {
|
|
||||||
readState = ReadState.READING;
|
|
||||||
errorReadCounter = 0;
|
|
||||||
errorResolvingCounter = 0;
|
|
||||||
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();
|
disconnect();
|
||||||
}
|
return null;
|
||||||
});
|
});
|
||||||
} else {
|
} else if (device.getConnectionState() == ConnectionState.CONNECTED && device.isServicesDiscovered()) {
|
||||||
errorReadCounter++;
|
// we can enable the notifications multiple times, this is handled internally
|
||||||
if (errorReadCounter < 6) {
|
enableNotifications(getServiceUUID(), getDataUUID()).thenAccept((v) -> {
|
||||||
logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
|
isNotifying.set(System.currentTimeMillis());
|
||||||
|
}).exceptionally((t) -> {
|
||||||
|
String message = "Failed to enable notifications on device " + address + ", disconnect";
|
||||||
|
logger.warn(message, t);
|
||||||
|
disconnect();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug("ERROR: Controller reset needed. Read data from device {} failed {} times", address,
|
logger.debug("Device {} state is {}, discovered {}", address, device.getConnectionState(),
|
||||||
errorReadCounter);
|
device.isServicesDiscovered());
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
|
||||||
"Read data from device failed");
|
|
||||||
}
|
}
|
||||||
disconnect();
|
} catch (Exception e) {
|
||||||
|
String message = "Failed to execute for device " + address;
|
||||||
|
logger.warn(message, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void discoverServices() {
|
|
||||||
logger.debug("Discover services for device {}", address);
|
|
||||||
serviceState = ServiceState.RESOLVING;
|
|
||||||
device.discoverServices();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onServicesDiscovered() {
|
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||||
serviceState = ServiceState.RESOLVED;
|
super.onCharacteristicUpdate(characteristic, value);
|
||||||
logger.debug("Service discovery completed for device {}", address);
|
|
||||||
printServices();
|
|
||||||
execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printServices() {
|
if (!getDataUUID().equals(characteristic.getUuid())) {
|
||||||
device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address, value);
|
||||||
|
updateChannels(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||||
logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
|
super.onConnectionStateChange(connectionNotification);
|
||||||
switch (connectionNotification.getConnectionState()) {
|
// stop sending triggers to a probably broken connection
|
||||||
case DISCONNECTED:
|
isNotifying.set(-1);
|
||||||
if (serviceState == ServiceState.RESOLVING) {
|
|
||||||
serviceState = ServiceState.NOT_RESOLVED;
|
|
||||||
}
|
|
||||||
readState = ReadState.IDLE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
if (connectionNotification.getConnectionState() == ConnectionState.CONNECTED) {
|
||||||
|
// start discovering when super.onConnectionStateChange does not
|
||||||
|
if (device.isServicesDiscovered() && !device.discoverServices()) {
|
||||||
|
logger.debug("Error while discovering services");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTimeToRead() {
|
|
||||||
int sinceLastRead = sinceLastReadSec.get();
|
|
||||||
logger.debug("Time since last update: {} sec", sinceLastRead);
|
|
||||||
return sinceLastRead >= configuration.refreshInterval;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,6 +141,13 @@ public abstract class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
|
|||||||
return configuration.fwVersion;
|
return configuration.fwVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the UUID of the service, which holds the characteristics
|
||||||
|
*
|
||||||
|
* @return the UUID of the data characteristic
|
||||||
|
*/
|
||||||
|
protected abstract UUID getServiceUUID();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the UUID of the characteristic, which holds the sensor data
|
* Provides the UUID of the characteristic, which holds the sensor data
|
||||||
*
|
*
|
||||||
@ -327,5 +174,5 @@ public abstract class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
|
|||||||
*
|
*
|
||||||
* @param is the content of the bluetooth characteristic
|
* @param is the content of the bluetooth characteristic
|
||||||
*/
|
*/
|
||||||
protected abstract void updateChannels(int[] is);
|
protected abstract void updateChannels(byte[] is);
|
||||||
}
|
}
|
||||||
|
@ -12,24 +12,12 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.bluetooth.radoneye.internal;
|
package org.openhab.binding.bluetooth.radoneye.internal;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.measure.Unit;
|
|
||||||
import javax.measure.quantity.Dimensionless;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||||
import org.openhab.core.library.dimension.Density;
|
|
||||||
import org.openhab.core.library.unit.SIUnits;
|
|
||||||
import org.openhab.core.library.unit.Units;
|
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
import tech.units.indriya.format.SimpleUnitFormat;
|
|
||||||
import tech.units.indriya.function.MultiplyConverter;
|
|
||||||
import tech.units.indriya.unit.ProductUnit;
|
|
||||||
import tech.units.indriya.unit.TransformedUnit;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link RadoneyeBindingConstants} class defines common constants, which are
|
* The {@link RadoneyeBindingConstants} class defines common constants, which are
|
||||||
* used across the whole binding.
|
* used across the whole binding.
|
||||||
@ -47,14 +35,5 @@ public class RadoneyeBindingConstants {
|
|||||||
|
|
||||||
// Channel IDs
|
// Channel IDs
|
||||||
public static final String CHANNEL_ID_RADON = "radon";
|
public static final String CHANNEL_ID_RADON = "radon";
|
||||||
|
public static final String CHANNEL_ID_DECAY = "decay";
|
||||||
public static final Unit<Dimensionless> PARTS_PER_BILLION = new TransformedUnit<>(Units.ONE,
|
|
||||||
MultiplyConverter.ofRational(BigInteger.ONE, BigInteger.valueOf(1000000000)));
|
|
||||||
public static final Unit<Density> BECQUEREL_PER_CUBIC_METRE = new ProductUnit<>(
|
|
||||||
Units.BECQUEREL.divide(SIUnits.CUBIC_METRE));
|
|
||||||
|
|
||||||
static {
|
|
||||||
SimpleUnitFormat.getInstance().label(PARTS_PER_BILLION, "ppb");
|
|
||||||
SimpleUnitFormat.getInstance().label(BECQUEREL_PER_CUBIC_METRE, "Bq/m³");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,9 @@ package org.openhab.binding.bluetooth.radoneye.internal;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link RadoneyeDataParser} is responsible for parsing data from Wave Plus device format.
|
* The {@link RadoneyeDataParser} is responsible for parsing data from Wave Plus device format.
|
||||||
@ -30,49 +27,40 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class RadoneyeDataParser {
|
public class RadoneyeDataParser {
|
||||||
public static final String RADON = "radon";
|
public static final String RADON = "radon";
|
||||||
|
|
||||||
|
public static final String DECAY = "decay";
|
||||||
|
|
||||||
private static final int EXPECTED_DATA_LEN_V1 = 20;
|
private static final int EXPECTED_DATA_LEN_V1 = 20;
|
||||||
private static final int EXPECTED_DATA_LEN_V2 = 12;
|
private static final int EXPECTED_DATA_LEN_V2 = 12;
|
||||||
private static final int EXPECTED_VER_PLUS = 1;
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(RadoneyeDataParser.class);
|
|
||||||
|
|
||||||
private RadoneyeDataParser() {
|
private RadoneyeDataParser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, Number> parseRd200Data(int fwVersion, int[] data) throws RadoneyeParserException {
|
public static Map<String, Number> parseRd200Data(int fwVersion, byte[] data) throws RadoneyeParserException {
|
||||||
LOGGER.debug("Parsed data length: {}", data.length);
|
|
||||||
LOGGER.debug("Parsed data: {}", data);
|
|
||||||
|
|
||||||
final Map<String, Number> result = new HashMap<>();
|
|
||||||
|
|
||||||
switch (fwVersion) {
|
switch (fwVersion) {
|
||||||
case 1:
|
case 1:
|
||||||
if (data.length != EXPECTED_DATA_LEN_V1) {
|
if (data.length != EXPECTED_DATA_LEN_V1) {
|
||||||
throw new RadoneyeParserException(String.format("Illegal data structure length '%d'", data.length));
|
throw new RadoneyeParserException(String.format("Illegal data structure length '%d'", data.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] radonArray = subArray(data, 2, 6);
|
byte[] radonArray = subArray(data, 2, 6);
|
||||||
result.put(RADON, new BigDecimal(readFloat(radonArray) * 37));
|
return Map.of(RADON, new BigDecimal(readFloat(radonArray) * 37));
|
||||||
break;
|
|
||||||
case 2:
|
case 2:
|
||||||
if (data.length != EXPECTED_DATA_LEN_V2) {
|
if (data.length != EXPECTED_DATA_LEN_V2) {
|
||||||
throw new RadoneyeParserException(String.format("Illegal data structure length '%d'", data.length));
|
throw new RadoneyeParserException(String.format("Illegal data structure length '%d'", data.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
result.put(RADON, intFromBytes(data[2], data[3]));
|
return Map.of(RADON, intFromBytes(data[2], data[3]), DECAY, intFromBytes(data[10], data[11]));
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException("fwVersion: " + fwVersion + " is not implemented");
|
throw new UnsupportedOperationException("fwVersion: " + fwVersion + " is not implemented");
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int intFromBytes(int lowByte, int highByte) {
|
private static int intFromBytes(byte lowByte, byte highByte) {
|
||||||
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
|
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Little endian
|
// Little endian
|
||||||
private static int fromByteArrayLE(int[] bytes) {
|
private static int fromByteArrayLE(byte[] bytes) {
|
||||||
int result = 0;
|
int result = 0;
|
||||||
for (int i = 0; i < bytes.length; i++) {
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
result |= (bytes[i] & 0xFF) << (8 * i);
|
result |= (bytes[i] & 0xFF) << (8 * i);
|
||||||
@ -80,12 +68,12 @@ public class RadoneyeDataParser {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float readFloat(int[] bytes) {
|
private static float readFloat(byte[] bytes) {
|
||||||
int i = fromByteArrayLE(bytes);
|
int i = fromByteArrayLE(bytes);
|
||||||
return Float.intBitsToFloat(i);
|
return Float.intBitsToFloat(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] subArray(int[] array, int beg, int end) {
|
private static byte[] subArray(byte[] array, int beg, int end) {
|
||||||
return Arrays.copyOfRange(array, beg, end + 1);
|
return Arrays.copyOfRange(array, beg, end + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,31 +85,40 @@ public class RadoneyeDiscoveryParticipant implements BluetoothDiscoveryParticipa
|
|||||||
|
|
||||||
private String getSerial(BluetoothDiscoveryDevice device) {
|
private String getSerial(BluetoothDiscoveryDevice device) {
|
||||||
String name = device.getName();
|
String name = device.getName();
|
||||||
|
if (name == null) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
String[] parts = name.split(":");
|
String[] parts = name.split(":");
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
return parts[2];
|
return parts[2];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "N/A";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getManufacturer(BluetoothDiscoveryDevice device) {
|
private String getManufacturer(BluetoothDiscoveryDevice device) {
|
||||||
String name = device.getName();
|
String name = device.getName();
|
||||||
|
if (name == null) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
String[] parts = name.split(":");
|
String[] parts = name.split(":");
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
return parts[0];
|
return parts[0];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "N/A";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getModel(BluetoothDiscoveryDevice device) {
|
private String getModel(BluetoothDiscoveryDevice device) {
|
||||||
String name = device.getName();
|
String name = device.getName();
|
||||||
|
if (name == null) {
|
||||||
|
return "N/A";
|
||||||
|
}
|
||||||
String[] parts = name.split(":");
|
String[] parts = name.split(":");
|
||||||
if (parts.length == 3) {
|
if (parts.length == 3) {
|
||||||
return parts[1];
|
return parts[1];
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "N/A";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.library.unit.Units;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -33,7 +34,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
public class RadoneyeHandler extends AbstractRadoneyeHandler {
|
public class RadoneyeHandler extends AbstractRadoneyeHandler {
|
||||||
|
|
||||||
private static final UUID SERVICE_UUID_V1 = UUID.fromString("00001523-1212-efde-1523-785feabcd123");
|
private static final UUID SERVICE_UUID_V1 = UUID.fromString("00001523-1212-efde-1523-785feabcd123");
|
||||||
private static final UUID SERVICE_UUID_V2 = UUID.fromString("00001524-0000-1000-8000-00805f9b34fb");
|
private static final UUID SERVICE_UUID_V2 = UUID.fromString("00001523-0000-1000-8000-00805f9b34fb");
|
||||||
private static final UUID TRIGGER_UID_V1 = UUID.fromString("00001524-1212-efde-1523-785feabcd123");
|
private static final UUID TRIGGER_UID_V1 = UUID.fromString("00001524-1212-efde-1523-785feabcd123");
|
||||||
private static final UUID TRIGGER_UID_V2 = UUID.fromString("00001524-0000-1000-8000-00805f9b34fb");
|
private static final UUID TRIGGER_UID_V2 = UUID.fromString("00001524-0000-1000-8000-00805f9b34fb");
|
||||||
private static final UUID DATA_UUID_V1 = UUID.fromString("00001525-1212-efde-1523-785feabcd123");
|
private static final UUID DATA_UUID_V1 = UUID.fromString("00001525-1212-efde-1523-785feabcd123");
|
||||||
@ -48,21 +49,40 @@ public class RadoneyeHandler extends AbstractRadoneyeHandler {
|
|||||||
private final Logger logger = LoggerFactory.getLogger(RadoneyeHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(RadoneyeHandler.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateChannels(int[] is) {
|
protected void updateChannels(byte[] is) {
|
||||||
Map<String, Number> data;
|
Map<String, Number> data;
|
||||||
try {
|
try {
|
||||||
|
logger.debug("Try to parse input from device: {}", is);
|
||||||
data = RadoneyeDataParser.parseRd200Data(getFwVersion(), is);
|
data = RadoneyeDataParser.parseRd200Data(getFwVersion(), is);
|
||||||
logger.debug("Parsed data: {}", data);
|
logger.debug("Parsed data: {}", data);
|
||||||
Number radon = data.get(RadoneyeDataParser.RADON);
|
Number radon = data.get(RadoneyeDataParser.RADON);
|
||||||
logger.debug("Parsed data radon number: {}", radon);
|
logger.debug("Parsed data radon number: {}", radon);
|
||||||
if (radon != null) {
|
if (radon != null) {
|
||||||
updateState(CHANNEL_ID_RADON, new QuantityType<>(radon, BECQUEREL_PER_CUBIC_METRE));
|
updateState(CHANNEL_ID_RADON, new QuantityType<>(radon, Units.BECQUEREL_PER_CUBIC_METRE));
|
||||||
|
}
|
||||||
|
Number decay = data.get(RadoneyeDataParser.DECAY);
|
||||||
|
logger.debug("Parsed data decay count: {}", decay);
|
||||||
|
if (decay != null) {
|
||||||
|
updateState(CHANNEL_ID_DECAY, new QuantityType<>(decay, Units.ONE));
|
||||||
}
|
}
|
||||||
} catch (RadoneyeParserException e) {
|
} catch (RadoneyeParserException e) {
|
||||||
logger.error("Failed to parse data received from Radoneye sensor: {}", e.getMessage());
|
logger.error("Failed to parse data received from Radoneye sensor: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected UUID getServiceUUID() {
|
||||||
|
int fwVersion = getFwVersion();
|
||||||
|
switch (fwVersion) {
|
||||||
|
case 1:
|
||||||
|
return SERVICE_UUID_V1;
|
||||||
|
case 2:
|
||||||
|
return SERVICE_UUID_V2;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("fwVersion: " + fwVersion + " is not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected UUID getDataUUID() {
|
protected UUID getDataUUID() {
|
||||||
int fwVersion = getFwVersion();
|
int fwVersion = getFwVersion();
|
||||||
|
@ -14,5 +14,7 @@ thing-type.config.bluetooth.radoneye_rd200.refreshInterval.description = States
|
|||||||
|
|
||||||
# channel types
|
# channel types
|
||||||
|
|
||||||
|
channel-type.bluetooth.radoneye_decay.label = Decay Counter
|
||||||
|
channel-type.bluetooth.radoneye_decay.description = The decay count in the last time frame
|
||||||
channel-type.bluetooth.radoneye_radon.label = Radon Current Level
|
channel-type.bluetooth.radoneye_radon.label = Radon Current Level
|
||||||
channel-type.bluetooth.radoneye_radon.description = Radon gas level
|
channel-type.bluetooth.radoneye_radon.description = Radon gas level
|
||||||
|
@ -17,8 +17,13 @@
|
|||||||
<channels>
|
<channels>
|
||||||
<channel id="rssi" typeId="rssi"/>
|
<channel id="rssi" typeId="rssi"/>
|
||||||
<channel id="radon" typeId="radoneye_radon"/>
|
<channel id="radon" typeId="radoneye_radon"/>
|
||||||
|
<channel id="decay" typeId="radoneye_decay"/>
|
||||||
</channels>
|
</channels>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<property name="thingTypeVersion">1</property>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<config-description>
|
<config-description>
|
||||||
<parameter name="address" type="text">
|
<parameter name="address" type="text">
|
||||||
<label>Address</label>
|
<label>Address</label>
|
||||||
@ -38,9 +43,15 @@
|
|||||||
</thing-type>
|
</thing-type>
|
||||||
|
|
||||||
<channel-type id="radoneye_radon">
|
<channel-type id="radoneye_radon">
|
||||||
<item-type>Number:Density</item-type>
|
<item-type>Number:RadiationSpecificActivity</item-type>
|
||||||
<label>Radon Current Level</label>
|
<label>Radon Current Level</label>
|
||||||
<description>Radon gas level</description>
|
<description>Radon gas level</description>
|
||||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
<channel-type id="radoneye_decay">
|
||||||
|
<item-type>Number:Dimensionless</item-type>
|
||||||
|
<label>Decay Counter</label>
|
||||||
|
<description>The decay count in the last time frame</description>
|
||||||
|
<state readOnly="true" pattern="%.0f"/>
|
||||||
|
</channel-type>
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||||
|
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||||
|
<thing-type uid="bluetooth:radoneye_rd200">
|
||||||
|
<instruction-set targetVersion="1">
|
||||||
|
<update-channel id="radon">
|
||||||
|
<type>bluetooth:radoneye_radon</type>
|
||||||
|
</update-channel>
|
||||||
|
<add-channel id="decay">
|
||||||
|
<type>bluetooth:radoneye_decay</type>
|
||||||
|
</add-channel>
|
||||||
|
</instruction-set>
|
||||||
|
</thing-type>
|
||||||
|
</update:update-descriptions>
|
@ -31,29 +31,35 @@ public class RadoneyeParserTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEmptyData() {
|
public void testEmptyData() {
|
||||||
int[] data = {};
|
byte[] data = {};
|
||||||
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(1, data));
|
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(1, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testWrongDataLen() throws RadoneyeParserException {
|
public void testWrongDataLen() throws RadoneyeParserException {
|
||||||
int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0 };
|
byte[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 127, 127, 2, 46, 0, 0 };
|
||||||
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(1, data));
|
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(1, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParsingRd200v1() throws RadoneyeParserException {
|
public void testParsingRd200v1() throws RadoneyeParserException {
|
||||||
int[] data = { 80, 16, 31, -123, 43, 64, 123, 20, 94, 64, 92, -113, -118, 64, 15, 0, 12, 0, 0, 0 };
|
byte[] data = { 80, 16, 31, -123, 43, 64, 123, 20, 94, 64, 92, -113, -118, 64, 15, 0, 12, 0, 0, 0 };
|
||||||
Map<String, Number> result = RadoneyeDataParser.parseRd200Data(1, data);
|
Map<String, Number> result = RadoneyeDataParser.parseRd200Data(1, data);
|
||||||
|
|
||||||
assertEquals(99, result.get(RadoneyeDataParser.RADON).intValue());
|
Number radon = assertInstanceOf(Number.class, result.get(RadoneyeDataParser.RADON));
|
||||||
|
assertEquals(99, radon.intValue());
|
||||||
|
assertFalse(result.containsKey(RadoneyeDataParser.DECAY));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParsingRd200v2() throws RadoneyeParserException {
|
public void testParsingRd200v2() throws RadoneyeParserException {
|
||||||
int[] data = { 0xff, 0xff, 0x5b, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
|
byte xFF = (byte) 0xff;
|
||||||
|
byte[] data = { xFF, xFF, 0x5b, 0x00, xFF, xFF, xFF, xFF, xFF, xFF, 0x42, 0x01 };
|
||||||
Map<String, Number> result = RadoneyeDataParser.parseRd200Data(2, data);
|
Map<String, Number> result = RadoneyeDataParser.parseRd200Data(2, data);
|
||||||
|
|
||||||
assertEquals(91, result.get(RadoneyeDataParser.RADON).intValue());
|
Number radon = assertInstanceOf(Number.class, result.get(RadoneyeDataParser.RADON));
|
||||||
|
assertEquals(91, radon.intValue());
|
||||||
|
Number decay = assertInstanceOf(Number.class, result.get(RadoneyeDataParser.DECAY));
|
||||||
|
assertEquals(322, decay.intValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user