Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 57 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,72 @@
# RadonReader
# RadonReader 2022 RD200 v2 (=>2022)

This project provides a tool which allows users collect current radon data from FTLab Radon Eye RD200 (Bluetooth only version).
This project provides a tool which allows users collect current radon data from FTLab Radon Eye RD200 v1 and V2 (2022+) (Bluetooth only versions).

EtoTen v0.4 - 07/05/2022
- Forked Project
- Changed compatability to Python3
- Added support for new RD200 models made in 2022
- Added auto-scan ability
- Change the read function to call the handler directly, instead of interacting with the UUIDs

# Hardware Requeriments
- FTLabs RadonEye RD200
- Raspberry Pi
- Bluetooth LE (Low Energy) support
Note: if specifying an (-a) MAC address, you now also have to specify a device type (-t) (either 0 for original RD200 or 1 for RD200 v2)

# Pre-req install steps:

# Software Requeriments
- Python 2.7.x
- bluepy Python library
<pre><code>sudo apt install libglib2.0-dev
pip3 install bluepy
pip3 install paho-mqtt
sudo setcap cap_net_raw+e /home/pi/.local/lib/python3.7/site-packages/bluepy/bluepy-helper
sudo setcap cap_net_admin+eip /home/pi/.local/lib/python3.7/site-packages/bluepy/bluepy-helper
</pre></code>

# Home assistant integration via MQTT:

- Install mosquitto MQTT add-on in HA, configure it with a local user and password
- Install mosquitto MQTT integration in HA
- On the host machine run: <pre><code>python3 radon_reader.py -v -ms localhost -mu radonuser -mw radon123 -ma -m</pre></code> and listen to:
"environment/RADONEYE/#" in the MQTT integration in HA to verify messages are being sent

- Now make the app send automatic updates to HA every 3 minutes
<pre><code>crontab -e</pre></code>
<pre><code>*/3 * * * * /usr/bin/python3 /home/pi/radonreader/radon_reader.py -v -a 94:3c:c6:dd:42:ce -t 1 -ms localhost -mu radonuser -mw radon123 -mw radon123 -ma -m #update radon reading via MQTT every 3 minutes</pre></code>

- Add this to configuration.yaml:
<pre><code>
mqtt:
sensor:
- state_topic: "environment/RADONEYE/#"
name: 'Radon Level'
unique_id: 'radon_level'
unit_of_measurement: 'pCi/L'
value_template: "{{ value_json.radonvalue }}"
</pre></code>

- Now you can add a sensor card to your HA view

# Hardware Requirements
- FTLabs RadonEye RD200 v1 or v2
- Raspberry Pi w/Bluetooth LE (Low Energy) support (RPi 3B/4/etc...)

# Software Requirements
- Python 3.7
- bluepy Python library
- paho-mqtt Python library

# History
- 0.4 - Forked and modified extensively
- 0.3 - Added MQTT support


# Usage
<pre><code>usage: radon_reader.py [-h] -a ADDRESS [-b] [-v] [-s] [-m] [-ms MQTT_SRV]
<pre><code>usage: radon_reader.py [-h] [-a] ADDRESS [-t] DEVICE_TYPE [-b] [-v] [-s] [-m] [-ms MQTT_SRV]
[-mp MQTT_PORT] [-mu MQTT_USER] [-mw MQTT_PW] [-ma]

RadonEye RD200 (Bluetooth/BLE) Reader

optional arguments:
-h, --help show this help message and exit
-a ADDRESS Bluetooth Address (AA:BB:CC:DD:EE:FF format)
-t TYPE 0 for original RD200, 1 for RD200 v2 (=>2022)
-b, --becquerel Display radon value in Becquerel (Bq/m^3) unit
-v, --verbose Verbose mode
-s, --silent Only output radon value (without unit and timestamp)
Expand All @@ -36,3 +76,9 @@ optional arguments:
-mu MQTT_USER MQTT server username
-mw MQTT_PW MQTT server password
-ma Enable Home Assistant MQTT output (Default: EmonCMS)</code></pre>

# Example usage:
<pre><code>python3 radon_reader.py -a 94:3c:c6:dd:42:ce -t 1 -v #verbose output/ specific device MAC
python3 radon_reader.py -v #verbose output, auto scan
python3 radon_reader.py -v -a 94:3c:c6:dd:42:ce -t 1 -ms localhost -mu radonuser -mw radon123 -ma -m #verbose output, specific device MAC, mqtt to home assistant
</pre></code>
Binary file added __pycache__/radon_reader_by_handle.cpython-37.pyc
Binary file not shown.
82 changes: 43 additions & 39 deletions radon_reader.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
#!/usr/bin/python
#!/usr/bin/python3

""" radon_reader.py: RadonEye RD200 (Bluetooth/BLE) Reader """

__progname__ = "RadonEye RD200 (Bluetooth/BLE) Reader"
__version__ = "0.3.8"
__author__ = "Carlos Andre"
__email__ = "candrecn at hotmail dot com"
__date__ = "2019-10-20"
__version__ = "0.4.0"
__author__ = "etoten"
__date__ = "2021-07-05"

import argparse, struct, time, re, json
import paho.mqtt.client as mqtt

import logging
import sys
from bluepy import btle
from time import sleep
from random import randint

from radon_reader_by_handle import radon_device_finder, radon_device_reader, radonDataRAW, ScanDelegate, ReadDelegate, nConnect

logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,description=__progname__)
parser.add_argument('-a',dest='address',help='Bluetooth Address (AA:BB:CC:DD:EE:FF format)',required=True)
parser.add_argument('-a',dest='address',help='Bluetooth Address (AA:BB:CC:DD:EE:FF format)',required=False)
parser.add_argument('-t',dest='type',help='rd200 type 0 for <2022 1 for =>2022 models',required=False)
parser.add_argument('-b','--becquerel',action='store_true',help='Display radon value in Becquerel (Bq/m^3) unit', required=False)
parser.add_argument('-v','--verbose',action='store_true',help='Verbose mode', required=False)
parser.add_argument('-s','--silent',action='store_true',help='Output only radon value (without unit and timestamp)', required=False)
Expand All @@ -28,53 +37,50 @@
parser.add_argument('-ma',dest='mqtt_ha',action='store_true',help='Switch to Home Assistant MQTT output (Default: EmonCMS)', required=False)
args = parser.parse_args()

args.address = args.address.upper()

if not re.match("^([0-9A-F]{2}:){5}[0-9A-F]{2}$", args.address) or (args.mqtt and (args.mqtt_srv == None or args.mqtt_user == None or args.mqtt_pw == None)):
if (args.mqtt and (args.mqtt_srv == None or args.mqtt_user == None or args.mqtt_pw == None)):
parser.print_help()
quit()

def GetRadonValue():
if args.verbose and not args.silent:
print ("Connecting...")
DevBT = btle.Peripheral(args.address, "random")
RadonEye = btle.UUID("00001523-1212-efde-1523-785feabcd123")
RadonEyeService = DevBT.getServiceByUUID(RadonEye)
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.ERROR)

if args.address != None and args.type != None:
args.address = args.address.upper()
if re.match("^([0-9A-F]{2}:){5}[0-9A-F]{2}$", args.address):
mRdDeviceAddress = args.address
mRdDeviceType = int(args.type)
else:
logger.info("-a (mac address) and -t (device type 0|1 not specified, reverting to auto-scan)")
mRdDeviceAddress, mRdDeviceType = radon_device_finder() #auto find the device
else:
logger.info("-a (mac address) and -t (device type 0|1 not specified, reverting to auto-scan)")
mRdDeviceAddress, mRdDeviceType = radon_device_finder() #auto find the device

# Write 0x50 to 00001524-1212-efde-1523-785feabcd123
if args.verbose and not args.silent:
print ("Writing...")
uuidWrite = btle.UUID("00001524-1212-efde-1523-785feabcd123")
RadonEyeWrite = RadonEyeService.getCharacteristics(uuidWrite)[0]
RadonEyeWrite.write(bytes("\x50"))
mRadonValueBQ, mRadonValuePCi = radon_device_reader (mRdDeviceAddress , mRdDeviceType) #get data from the device

# Read from 3rd to 6th byte of 00001525-1212-efde-1523-785feabcd123
if args.verbose and not args.silent:
print ("Reading...")
uuidRead = btle.UUID("00001525-1212-efde-1523-785feabcd123")
RadonEyeValue = RadonEyeService.getCharacteristics(uuidRead)[0]
RadonValue = RadonEyeValue.read()
RadonValue = struct.unpack('<f',RadonValue[2:6])[0]

DevBT.disconnect()

# Raise exception (will try get Radon value from RadonEye again) if received a very
# high radon value or lower than 0.
# Maybe a bug on RD200 or Python BLE Lib?!
if ( RadonValue > 1000 ) or ( RadonValue < 0 ):
if ( mRadonValueBQ > 1000 ) or ( mRadonValueBQ < 0 ):
raise Exception("Very strange radon value. Debugging needed.")

if args.becquerel:
Unit="Bq/m^3"
RadonValue = ( RadonValue * 37 )
RadonValue = mRadonValueBQ
else:
Unit="pCi/L"

RadonValue = mRadonValuePCi


if args.silent:
print ("%0.2f" % (RadonValue))
else:
print ("%s - %s - Radon Value: %0.2f %s" % (time.strftime("%Y-%m-%d [%H:%M:%S]"),args.address,RadonValue,Unit))
else:
print ("%s - %s - Radon Value: %0.2f %s" % (time.strftime("%Y-%m-%d [%H:%M:%S]"),mRdDeviceAddress,RadonValue,Unit))

if args.mqtt:
if args.verbose and not args.silent:
print ("Sending to MQTT...")
Expand Down Expand Up @@ -109,12 +115,10 @@ def GetRadonValue():
except Exception as e:
if args.verbose and not args.silent:
print (e)

for i in range(1,4):
try:
if args.verbose and not args.silent:
print ("Failed, trying again (%s)..." % i)

logger.debug("Failed, trying again (%s)..." % i)
sleep(5)
GetRadonValue()

Expand Down
121 changes: 121 additions & 0 deletions radon_reader_by_handle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/python3
import bluepy.btle as btle
from bluepy.btle import Scanner, DefaultDelegate, BTLEException
import struct
import logging
import sys
from time import sleep


logger = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.setLevel(logging.DEBUG)


class ScanDelegate(DefaultDelegate):
def __init__(self):
DefaultDelegate.__init__(self)

def handleDiscovery(self, dev, isNewDev, isNewData):
pass

def radon_device_finder():
logger.debug('Scanning for devices')
scanner = Scanner().withDelegate(ScanDelegate())
try:
devices = scanner.scan(10.0)
for device in devices:
name = ''
if device.getValueText(9):
name = device.getValueText(9)
elif device.getValueText(8):
name = device.getValueText(8)
logger.debug('Device name: ' + name)
if 'FR:RU' in name:
logger.info('Found RD200 - x>=2022 revision with address: ' + device.addr)
return device.addr, 1
elif 'FR:R2' in name:
logger.info('Found RD200 - x<2002 revision with address: ' + device.addr)
return device.addr, 0
logger.info('Finished scanning for devices, no devices found')
return "", -1
except BTLEException as e:
scanner.stop()
logger.error('Recieved scan exception ' + e.message)

#raw data for .05 pCi/L:
##50 0a 02 00 05 00 00 00 00 00 00 00
##02 05 00 00 05 00 00 00 00'
##b'P\n\x02\x00\x05\x00\x00\x00\x00\x00\x00\x00'

radonDataRAW = b"\x00"
class ReadDelegate(btle.DefaultDelegate):
def handleNotification(self, cHandle, data):
global radonDataRAW
logger.debug('Radon Value Raw: {}'.format(data))
radonDataRAW=data

def nConnect(per, num_retries, address):
try_num = 1
while (try_num < num_retries):
try:
per._connect(address)
return True
except BTLEException:
logger.debug("Re-trying connections attempts: {}'".format(try_num))
try_num += 1
sleep(1)
# if we fell through the while loop, it failed to connect
return False

def radon_device_reader(rdDeviceAddress,rdDeviceType):
if rdDeviceType >= 0:
p = btle.Peripheral()
nConnect(p, 5, rdDeviceAddress);
p.withDelegate(ReadDelegate())
#send -- "char-write-cmd 0x002a 50\r" ./temp_expect.sh (https://community.home-assistant.io/t/radoneye-ble-interface/94962/115)

if rdDeviceType == 1:
#handle: 0x002a, uuid: 00001524-0000-1000-8000-00805f9b34fb
intHandle = int.from_bytes(b'\x00\x2a', "big")
elif rdDeviceType == 0: #old
#handle: 0x000b, uuid: 00001524-1212-efde-1523-785feabcd123
intHandle = int.from_bytes(b'\x00\x0b', "big")

bGETValues = b"\x50"

logger.debug('Sending payload (byte): %s To handle (int): %s', bGETValues, intHandle)
p.writeCharacteristic(intHandle, bGETValues, True);
while p.waitForNotifications(1):
pass
p.disconnect()

if rdDeviceType == 1: #new RD200 sends back short int (2-bytes) with Bq/m^3
RadonValueBQ = struct.unpack('<H',radonDataRAW[2:4])[0]
RadonValuePCi = ( RadonValueBQ / 37 )
elif rdDeviceType == 0: #old RD200 sends back pCi/L as a 4-byte float
RadonValuePCi = struct.unpack('<f',radonDataRAW[2:6])[0]
RadonValueBQ = ( RadonValuePCi * 37 )
logger.info('Radon Value Bq/m^3: {}'.format(RadonValueBQ))
logger.info('Radon Value pCi/L: {}'.format(RadonValuePCi))

return RadonValueBQ, RadonValuePCi

elif rdDeviceType == -1:
logger.error("Device not found, no data to return.")
return -1,-1


#testing w/o radon_reader.py

if ('radon_reader_by_handle' not in sys.modules): #if I was not imported

logger.addHandler(handler) # add handler for standard out

mRdDeviceAddress, mRdDeviceType = radon_device_finder() #auto find the device
radon_device_reader (mRdDeviceAddress , mRdDeviceType) #get data from the device


Loading