diff --git a/README.md b/README.md index c5e5ce1..3866c60 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,64 @@ -# 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 +
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
+
+ +# 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:
python3 radon_reader.py -v -ms localhost -mu radonuser -mw radon123  -ma -m
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 +
crontab -e
+
*/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
+ +- Add this to configuration.yaml: +

+mqtt:
+  sensor:
+    - state_topic: "environment/RADONEYE/#"
+      name: 'Radon Level'
+      unique_id: 'radon_level'
+      unit_of_measurement: 'pCi/L'
+      value_template: "{{ value_json.radonvalue }}"
+
+ +- 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 -
usage: radon_reader.py [-h] -a ADDRESS [-b] [-v] [-s] [-m] [-ms MQTT_SRV]
+
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
@@ -27,6 +66,7 @@ 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)
@@ -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)
+ +# Example usage: +
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
+
diff --git a/__pycache__/radon_reader_by_handle.cpython-37.pyc b/__pycache__/radon_reader_by_handle.cpython-37.pyc new file mode 100644 index 0000000..ca80490 Binary files /dev/null and b/__pycache__/radon_reader_by_handle.cpython-37.pyc differ diff --git a/radon_reader.py b/radon_reader.py index 64f2a0a..50f699a 100644 --- a/radon_reader.py +++ b/radon_reader.py @@ -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) @@ -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(' 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...") @@ -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() diff --git a/radon_reader_by_handle.py b/radon_reader_by_handle.py new file mode 100644 index 0000000..22e4e6a --- /dev/null +++ b/radon_reader_by_handle.py @@ -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(' notification receive on: 0x002c +if {$DEVICE_TYPE == "1"} { + set SEND_HANDLE 0x002a + set RECIEVE_HANDLE 0x002c +} + +##Old device: +##send 0x000b -> notification receive on: 0x000d +if {$DEVICE_TYPE == "0" } { + set SEND_HANDLE 0x000b + set RECIEVE_HANDLE 0x000d +} + +spawn gatttool -b $MAC_A -I +# +match_max 100000 +# +expect "> " +# +while (1) { + send -- "connect\r" + expect "Connection successful" break + sleep 1 +# puts "next attempt\n" +} +# +send -- "mtu 507\r" +expect "MTU was exchanged successfully" { + sleep 1 + send -- "char-write-cmd $SEND_HANDLE $COMMAND\r" + + + set systemTime [clock seconds] + puts "Time [clock format $systemTime -format %Y-%m-%dT%H:%M:%S]" + set message1 "" + + expect { + -re "Notification handle = $RECIEVE_HANDLE value: (.+\n)" { + set message1 ${message1}$expect_out(1,string) + exp_continue + } + } + puts "$message1" + } + +send -- "exit\r" +# +expect eof