diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index d74ffb24082..efe39a63389 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -206,6 +206,11 @@
org.openhab.binding.bluetooth.govee
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.bluetooth.radoneye
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.bluetooth.roaming
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/NOTICE b/bundles/org.openhab.binding.bluetooth.radoneye/NOTICE
new file mode 100644
index 00000000000..38d625e3492
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/README.md b/bundles/org.openhab.binding.bluetooth.radoneye/README.md
new file mode 100644
index 00000000000..98e9cb2c5b2
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/README.md
@@ -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" }
+```
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/pom.xml b/bundles/org.openhab.binding.bluetooth.radoneye/pom.xml
new file mode 100644
index 00000000000..ad9bbd82395
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 4.0.0-SNAPSHOT
+
+
+ org.openhab.binding.bluetooth.radoneye
+
+ openHAB Add-ons :: Bundles :: RadonEye Bluetooth Adapter
+
+
+
+ org.openhab.addons.bundles
+ org.openhab.binding.bluetooth
+ ${project.version}
+ provided
+
+
+
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/feature/feature.xml b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/feature/feature.xml
new file mode 100644
index 00000000000..fa7af0f740e
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/feature/feature.xml
@@ -0,0 +1,12 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-serial
+ mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}
+ mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.radoneye/${project.version}
+
+
+
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/AbstractRadoneyeHandler.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/AbstractRadoneyeHandler.java
new file mode 100644
index 00000000000..7b3497865fa
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/AbstractRadoneyeHandler.java
@@ -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 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);
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeBindingConstants.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeBindingConstants.java
new file mode 100644
index 00000000000..bfb97abe1f5
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeBindingConstants.java
@@ -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 SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_RADONEYE);
+
+ // Channel IDs
+ public static final String CHANNEL_ID_RADON = "radon";
+
+ public static final Unit PARTS_PER_BILLION = new TransformedUnit<>(Units.ONE,
+ MultiplyConverter.ofRational(BigInteger.ONE, BigInteger.valueOf(1000000000)));
+ public static final Unit 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³");
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeConfiguration.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeConfiguration.java
new file mode 100644
index 00000000000..5ee2d7a2f55
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeConfiguration.java
@@ -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 + "]";
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDataParser.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDataParser.java
new file mode 100644
index 00000000000..730834a40ae
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDataParser.java
@@ -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 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 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDiscoveryParticipant.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDiscoveryParticipant.java
new file mode 100644
index 00000000000..decf69417ae
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeDiscoveryParticipant.java
@@ -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 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 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();
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandler.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandler.java
new file mode 100644
index 00000000000..a5b36a8b0f7
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandler.java
@@ -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 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(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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandlerFactory.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandlerFactory.java
new file mode 100644
index 00000000000..950ddcc65e3
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeHandlerFactory.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeParserException.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeParserException.java
new file mode 100644
index 00000000000..b8861f1051c
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/java/org/openhab/binding/bluetooth/radoneye/internal/RadoneyeParserException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/main/resources/OH-INF/thing/radoneye.xml b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/resources/OH-INF/thing/radoneye.xml
new file mode 100644
index 00000000000..4992c2a9c59
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/main/resources/OH-INF/thing/radoneye.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+ Indoor radon monitor
+
+
+
+
+
+
+
+
+
+ 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:Density
+
+ Radon gas level
+
+
+
diff --git a/bundles/org.openhab.binding.bluetooth.radoneye/src/test/java/org/openhab/binding/bluetooth/radoneye/RadoneyeParserTest.java b/bundles/org.openhab.binding.bluetooth.radoneye/src/test/java/org/openhab/binding/bluetooth/radoneye/RadoneyeParserTest.java
new file mode 100644
index 00000000000..47b0c68f0c7
--- /dev/null
+++ b/bundles/org.openhab.binding.bluetooth.radoneye/src/test/java/org/openhab/binding/bluetooth/radoneye/RadoneyeParserTest.java
@@ -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 result = RadoneyeDataParser.parseRd200Data(data);
+
+ assertEquals(99, result.get(RadoneyeDataParser.RADON).intValue());
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index acbacfc1bec..e22d555988c 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -75,6 +75,7 @@
org.openhab.binding.bluetooth.enoceanble
org.openhab.binding.bluetooth.generic
org.openhab.binding.bluetooth.govee
+ org.openhab.binding.bluetooth.radoneye
org.openhab.binding.bluetooth.roaming
org.openhab.binding.bluetooth.ruuvitag
org.openhab.binding.bondhome
diff --git a/features/openhab-addons/src/main/resources/footer.xml b/features/openhab-addons/src/main/resources/footer.xml
index d38fbdf3ff2..61351094f07 100644
--- a/features/openhab-addons/src/main/resources/footer.xml
+++ b/features/openhab-addons/src/main/resources/footer.xml
@@ -13,6 +13,7 @@
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.enoceanble/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}
+ mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.radoneye/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}
mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}