[bluetooth] Add support for RadonEye (BLE) device (#11958)

Signed-off-by: Peter Obel <peter@ecomerc.com>
This commit is contained in:
petero-dk 2023-02-27 18:59:50 +01:00 committed by GitHub
parent 03e3b6aae2
commit e9a1dd5b27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1011 additions and 0 deletions

View File

@ -206,6 +206,11 @@
<artifactId>org.openhab.binding.bluetooth.govee</artifactId> <artifactId>org.openhab.binding.bluetooth.govee</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bluetooth.radoneye</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bluetooth.roaming</artifactId> <artifactId>org.openhab.binding.bluetooth.roaming</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,47 @@
# radoneye
This extension adds support for [RadonEye](http://radonftlab.com/radon-sensor-product/radon-detector/rd200/) radon bluetooth detector.
## Supported Things
Following thing types are supported by this extension:
| Thing Type ID | Description |
| ------------------- | -------------------------------------- |
| radoneye_rd200 | Original RadonEye (RD200) |
## Discovery
As any other Bluetooth device, RadonEye devices are discovered automatically by the corresponding bridge.
## Thing Configuration
Supported configuration parameters for the things:
| Property | Type | Default | Required | Description |
|---------------------------------|---------|---------|----------|-----------------------------------------------------------------|
| address | String | | Yes | Bluetooth address of the device (in format "XX:XX:XX:XX:XX:XX") |
| refreshInterval | Integer | 300 | No | How often a refresh shall occur in seconds |
## Channels
Following channels are supported for `RadonEye` thing:
| Channel ID | Item Type | Description |
| ------------------ | ------------------------ | ------------------------------------------- |
| radon | Number:Density | The measured radon level |
## Example
radoneye.things (assuming you have a Bluetooth bridge with the ID `bluetooth:bluegiga:adapter1`:
```
bluetooth:radoneye_rd200:adapter1:sensor1 "radoneye Wave Plus Sensor 1" (bluetooth:bluegiga:adapter1) [ address="12:34:56:78:9A:BC", refreshInterval=300 ]
```
radoneye.items:
```
Number:Density radon "Radon level [%d %unit%]" { channel="bluetooth:radoneye_rd200:adapter1:sensor1:radon" }
```

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.bluetooth.radoneye</artifactId>
<name>openHAB Add-ons :: Bundles :: RadonEye Bluetooth Adapter</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bluetooth</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bluetooth.radoneye-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-bluetooth-radoneye" description="Bluetooth Binding RadonEye" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.radoneye/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,326 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.BluetoothUtils;
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
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.LoggerFactory;
/**
* The {@link AbstractRadoneyeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
abstract public class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
private static final int CHECK_PERIOD_SEC = 10;
private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
private AtomicInteger sinceLastReadSec = new AtomicInteger();
private Optional<RadoneyeConfiguration> configuration = Optional.empty();
private @Nullable ScheduledFuture<?> scheduledTask;
private volatile int refreshInterval;
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) {
super(thing);
}
@Override
public void initialize() {
logger.debug("Initialize");
super.initialize();
configuration = Optional.of(getConfigAs(RadoneyeConfiguration.class));
logger.debug("Using configuration: {}", configuration.get());
cancelScheduledTask();
configuration.ifPresent(cfg -> {
refreshInterval = cfg.refreshInterval;
logger.debug("Start scheduled task to read device in every {} seconds", refreshInterval);
scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
TimeUnit.SECONDS);
});
sinceLastReadSec.set(refreshInterval); // update immediately
}
@Override
public void dispose() {
logger.debug("Dispose");
cancelScheduledTask();
serviceState = ServiceState.NOT_RESOLVED;
readState = ReadState.IDLE;
super.dispose();
}
private void cancelScheduledTask() {
if (scheduledTask != null) {
scheduledTask.cancel(true);
scheduledTask = null;
}
}
private void executePeridioc() {
sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
execute();
}
private synchronized void execute() {
ConnectionState connectionState = device.getConnectionState();
logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
readState);
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();
}
});
} else {
errorReadCounter++;
if (errorReadCounter < 6) {
logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
} else {
logger.debug("ERROR: Controller reset needed. Read data from device {} failed {} times", address,
errorReadCounter);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Read data from device failed");
}
disconnect();
}
}
private void discoverServices() {
logger.debug("Discover services for device {}", address);
serviceState = ServiceState.RESOLVING;
device.discoverServices();
}
@Override
public void onServicesDiscovered() {
serviceState = ServiceState.RESOLVED;
logger.debug("Service discovery completed for device {}", address);
printServices();
execute();
}
private void printServices() {
device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
}
@Override
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
switch (connectionNotification.getConnectionState()) {
case DISCONNECTED:
if (serviceState == ServiceState.RESOLVING) {
serviceState = ServiceState.NOT_RESOLVED;
}
readState = ReadState.IDLE;
break;
default:
break;
}
execute();
}
private boolean isTimeToRead() {
int sinceLastRead = sinceLastReadSec.get();
logger.debug("Time since last update: {} sec", sinceLastRead);
return sinceLastRead >= refreshInterval;
}
/**
* Provides the UUID of the characteristic, which holds the sensor data
*
* @return the UUID of the data characteristic
*/
protected abstract UUID getDataUUID();
/**
* Provides the UUID of the characteristic, that triggers and update of the sensor data
*
* @return the UUID of the data characteristic
*/
protected abstract UUID getTriggerUUID();
/**
* Provides the data that sent to the trigger characteristic will update the sensor data
*
* @return the trigger data as an byte array
*/
protected abstract byte[] getTriggerData();
/**
* This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
*
* @param is the content of the bluetooth characteristic
*/
abstract protected void updateChannels(int[] is);
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import java.math.BigInteger;
import java.util.Set;
import javax.measure.Unit;
import javax.measure.quantity.Dimensionless;
import org.eclipse.jdt.annotation.NonNullByDefault;
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 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
* used across the whole binding.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_RADONEYE = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
"radoneye_rd200");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_RADONEYE);
// Channel IDs
public static final String CHANNEL_ID_RADON = "radon";
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³");
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Configuration class for {@link RadoneyeBinding} device.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeConfiguration {
public String address = "";
public int refreshInterval;
@Override
public String toString() {
return "[address=" + address + ", refreshInterval=" + refreshInterval + "]";
}
}

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
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.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeDataParser {
public static final String RADON = "radon";
private static final int EXPECTED_DATA_LEN = 20;
private static final int EXPECTED_VER_PLUS = 1;
private static final Logger logger = LoggerFactory.getLogger(RadoneyeDataParser.class);
private RadoneyeDataParser() {
}
public static Map<String, Number> parseRd200Data(int[] data) throws RadoneyeParserException {
logger.debug("Parsed data length: {}", data.length);
logger.debug("Parsed data: {}", data);
if (data.length == EXPECTED_DATA_LEN) {
final Map<String, Number> result = new HashMap<>();
int[] radonArray = subArray(data, 2, 6);
result.put(RADON, new BigDecimal(readFloat(radonArray) * 37));
return result;
} else {
throw new RadoneyeParserException(String.format("Illegal data structure length '%d'", data.length));
}
}
private static int intFromBytes(int lowByte, int highByte) {
return (highByte & 0xFF) << 8 | (lowByte & 0xFF);
}
// Little endian
private static int fromByteArrayLE(int[] bytes) {
int result = 0;
for (int i = 0; i < bytes.length; i++) {
result |= (bytes[i] & 0xFF) << (8 * i);
}
return result;
}
private static float readFloat(int[] bytes) {
int i = fromByteArrayLE(bytes);
return Float.intBitsToFloat(i);
}
private static int[] subArray(int[] array, int beg, int end) {
return Arrays.copyOfRange(array, beg, end + 1);
}
}

View File

@ -0,0 +1,155 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
/**
* This discovery participant is able to recognize RadonEye devices and create discovery results for them.
*
* @author Peter Obel - Initial contribution
*
*/
@NonNullByDefault
@Component
public class RadoneyeDiscoveryParticipant implements BluetoothDiscoveryParticipant {
private static final String RADONEYE_BLUETOOTH_COMPANY_ID = "f24be3";
private static final String RD200 = "R20"; // RadonEye First Generation BLE
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return RadoneyeBindingConstants.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
if (isRadoneyeDevice(device)) {
if (RD200.equals(getModel(device))) {
return new ThingUID(RadoneyeBindingConstants.THING_TYPE_RADONEYE, device.getAdapter().getUID(),
device.getAddress().toString().toLowerCase().replace(":", ""));
}
}
return null;
}
@Override
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
if (!isRadoneyeDevice(device)) {
return null;
}
ThingUID thingUID = getThingUID(device);
if (thingUID == null) {
return null;
}
if (RD200.equals(getModel(device))) {
return createResult(device, thingUID, "RadonEye (BLE)");
}
return null;
}
@Override
public boolean requiresConnection(BluetoothDiscoveryDevice device) {
return isRadoneyeDevice(device);
}
private boolean isRadoneyeDevice(BluetoothDiscoveryDevice device) {
String manufacturerMacId = device.getAddress().toString().toLowerCase().replace(":", "").substring(0, 6);
if (manufacturerMacId.equals(RADONEYE_BLUETOOTH_COMPANY_ID.toLowerCase())) {
return true;
}
return false;
}
private String getSerial(BluetoothDiscoveryDevice device) {
String name = device.getName();
String[] parts = name.split(":");
if (parts.length == 3) {
return parts[2];
} else {
return "";
}
}
private String getManufacturer(BluetoothDiscoveryDevice device) {
String name = device.getName();
String[] parts = name.split(":");
if (parts.length == 3) {
return parts[0];
} else {
return "";
}
}
private String getModel(BluetoothDiscoveryDevice device) {
String name = device.getName();
String[] parts = name.split(":");
if (parts.length == 3) {
return parts[1];
} else {
return "";
}
}
private DiscoveryResult createResult(BluetoothDiscoveryDevice device, ThingUID thingUID, String label) {
Map<String, Object> properties = new HashMap<>();
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
properties.put(Thing.PROPERTY_VENDOR, "RadonEye");
String name = device.getName();
String serialNumber = device.getSerialNumber();
String firmwareRevision = device.getFirmwareRevision();
String model = device.getModel();
String hardwareRevision = device.getHardwareRevision();
Integer txPower = device.getTxPower();
if (serialNumber != null) {
properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
} else {
properties.put(Thing.PROPERTY_MODEL_ID, getSerial(device));
}
if (firmwareRevision != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareRevision);
}
if (model != null) {
properties.put(Thing.PROPERTY_MODEL_ID, model);
} else {
properties.put(Thing.PROPERTY_MODEL_ID, getModel(device));
}
if (hardwareRevision != null) {
properties.put(Thing.PROPERTY_HARDWARE_VERSION, hardwareRevision);
}
if (txPower != null) {
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
}
properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getAddress().toString());
// Create the discovery result and add to the inbox
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import static org.openhab.binding.bluetooth.radoneye.internal.RadoneyeBindingConstants.*;
import java.util.Map;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.dimension.Density;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RadoneyeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeHandler extends AbstractRadoneyeHandler {
private static final String SERVICE_UUID = "00001523-1212-efde-1523-785feabcd123";
private static final String TRIGGER_UID = "00001524-1212-efde-1523-785feabcd123";
private static final String DATA_UUID = "00001525-1212-efde-1523-785feabcd123";
public RadoneyeHandler(Thing thing) {
super(thing);
}
private final Logger logger = LoggerFactory.getLogger(RadoneyeHandler.class);
private final UUID dataUuid = UUID.fromString(DATA_UUID);
private final UUID triggerUuid = UUID.fromString(TRIGGER_UID);
private final byte[] triggerData = new byte[] { 0x50 };
@Override
protected void updateChannels(int[] is) {
Map<String, Number> data;
try {
data = RadoneyeDataParser.parseRd200Data(is);
logger.debug("Parsed data: {}", data);
Number radon = data.get(RadoneyeDataParser.RADON);
logger.debug("Parsed data radon number: {}", radon);
if (radon != null) {
updateState(CHANNEL_ID_RADON, new QuantityType<Density>(radon, BECQUEREL_PER_CUBIC_METRE));
}
} catch (RadoneyeParserException e) {
logger.error("Failed to parse data received from Radoneye sensor: {}", e.getMessage());
}
}
@Override
protected UUID getDataUUID() {
return dataUuid;
}
@Override
protected UUID getTriggerUUID() {
return triggerUuid;
}
@Override
protected byte[] getTriggerData() {
return triggerData;
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link RadoneyeHandlerFactory} is responsible for creating things and thing handlers.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.radoneye")
public class RadoneyeHandlerFactory extends BaseThingHandlerFactory {
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return RadoneyeBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(RadoneyeBindingConstants.THING_TYPE_RADONEYE)) {
return new RadoneyeHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2023 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.radoneye.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for data parsing errors.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeParserException extends Exception {
private static final long serialVersionUID = 1;
public RadoneyeParserException() {
}
public RadoneyeParserException(String message) {
super(message);
}
public RadoneyeParserException(String message, Throwable cause) {
super(message, cause);
}
public RadoneyeParserException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bluetooth"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="radoneye_rd200">
<supported-bridge-type-refs>
<bridge-type-ref id="roaming"/>
<bridge-type-ref id="bluegiga"/>
<bridge-type-ref id="bluez"/>
</supported-bridge-type-refs>
<label>RadonEye RD200</label>
<description>Indoor radon monitor</description>
<channels>
<channel id="rssi" typeId="rssi"/>
<channel id="radon" typeId="radoneye_radon"/>
</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="radoneye_radon">
<item-type>Number:Density</item-type>
<label>Radon Current Level</label>
<description>Radon gas level</description>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2023 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.radoneye;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.bluetooth.radoneye.internal.RadoneyeDataParser;
import org.openhab.binding.bluetooth.radoneye.internal.RadoneyeParserException;
/**
* Tests {@link RadoneyeParserTest}.
*
* @author Peter Obel - Initial contribution
*/
@NonNullByDefault
public class RadoneyeParserTest {
@Test
public void testEmptyData() {
int[] data = {};
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(data));
}
@Test
public void testWrongDataLen() throws RadoneyeParserException {
int[] data = { 1, 55, 51, 0, 122, 0, 61, 0, 119, 9, 11, 194, 169, 2, 46, 0, 0 };
assertThrows(RadoneyeParserException.class, () -> RadoneyeDataParser.parseRd200Data(data));
}
@Test
public void testParsingRd200() throws RadoneyeParserException {
int[] 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(data);
assertEquals(99, result.get(RadoneyeDataParser.RADON).intValue());
}
}

View File

@ -75,6 +75,7 @@
<module>org.openhab.binding.bluetooth.enoceanble</module> <module>org.openhab.binding.bluetooth.enoceanble</module>
<module>org.openhab.binding.bluetooth.generic</module> <module>org.openhab.binding.bluetooth.generic</module>
<module>org.openhab.binding.bluetooth.govee</module> <module>org.openhab.binding.bluetooth.govee</module>
<module>org.openhab.binding.bluetooth.radoneye</module>
<module>org.openhab.binding.bluetooth.roaming</module> <module>org.openhab.binding.bluetooth.roaming</module>
<module>org.openhab.binding.bluetooth.ruuvitag</module> <module>org.openhab.binding.bluetooth.ruuvitag</module>
<module>org.openhab.binding.bondhome</module> <module>org.openhab.binding.bondhome</module>

View File

@ -13,6 +13,7 @@
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.enoceanble/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.enoceanble/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.radoneye/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle>
</feature> </feature>