RadonEye BLE Interface

Thank you for your help. I probably never would have gotten anywhere if you hadn’t came up with this information.

Oh, FYI the Radon values (peak, 1-day average, and current) will probably be one byte with a value > 0, followed by x00. Unless you have really high Radon levels (> 6.89 pCi/L). At least if it’s using short ints for these as well, which I think it is. My response only contained 2 possible 4-byte int values, and there are 3 Radon levels reported.

Oh, you can also look at char-write-cmd 0x002a 50 and char-write-cmd 0x002a 51 but I don’t think those are the current data values. I’m pretty sure it’s x40 for requesting current data.

EDIT: I’m pretty sure I found the peak value in x40: bytes 52 and 53 gave me x63 x00, which converts to 99 Bq/m^3, which matched my peak value of 2.68 pCi/L. I need to watch that response over time to match my current reading… Maybe I’ll be back with more info.

I think bytes 34-35 in the x40 response contain the current value (x2e x00 matches 46 Bq/m^3, matches 1.24 pCi/L shown on my RD200).

One last thing: don’t have the app open when you are running any other connection tool, or it won’t connect. I’d run gatttool, get the results, disconnect, then run the app to compare with current data.

The time could be off by one minute as long as you do it pretty quickly, but as long as it doesn’t end up right on a 10-minute measurement time (e.g. 3d 02h 20m, for example), the Radon values will all be the same. The radon values only change every 10 minutes.

I’ll make sure I disconnect after getting the data, long term I don’t plan on using the app at all but short term I definitely will need to compare the data to get it context.

Most of what you guys have said is over my head but if there is anything I can do to help either test or get the ESPHome integration working with the new RD200s let me know. I bought one this last week hoping to integrate it with HA and now here I am :slight_smile:

1 Like

I looked at the code in radon_reader.py, and then looked at what my RD200 returned.

I found that sending x50 returns some data that also matches my numbers. However, it’s different from what is expected in that code. Here’s part of what’s in the radon_reader.py code, with old code commented and new code added that should work with the newer devices:

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")
    RadonEye = btle.UUID("00001523-0000-1000-8000-00805f9b34fb")
    RadonEyeService = DevBT.getServiceByUUID(RadonEye)

    # 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")
    uuidWrite  = btle.UUID("00001524-0000-1000-8000-00805f9b34fb")
    RadonEyeWrite = RadonEyeService.getCharacteristics(uuidWrite)[0]
    RadonEyeWrite.write(bytes("\x50"))

    # 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")
    uuidRead  = btle.UUID("00001525-0000-1000-8000-00805f9b34fb")
    RadonEyeValue = RadonEyeService.getCharacteristics(uuidRead)[0]
    RadonValue = RadonEyeValue.read()
#    RadonValue = struct.unpack('<f',RadonValue[2:6])[0]
    RadonValue = struct.unpack('<H',RadonValue[2:4])[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 ):
        raise Exception("Very strange radon value. Debugging needed.")

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

What I would need to do is query the Name and find out if it’s FR:RU222 or FR:R20:SN, then use the correct read/write statements and use the correct units. I’m not sure if someone with a new RadonEye can test these updates in RadonReader, and if @jeffeb3 can make these changes in radon_eye_RD200 based on get_name() in parse_device() for the radon_eye_ble component.

I’m not using ESPHome, so I probably won’t be able to make it work here. However, I might be able to get RadonReader.py to work. Right now I have something working using pygatt instead of bluepy in a Python script, but it needs work. Not sure if I should update radon_reader.py or just fix my horrible code. For now it’s running OK every 15 minutes on my RPi, unless bluetooth crashes.

Hey I tried your above modification to radon_reader.py and it does not work for me on the new RD200. If you have a complete code in pygatt or radon_reader.py that you could post, that would be helpful and I could clean it up for you.

1 Like

There was an expect script by brinap some days ago.
I modified the script and call it from crontab (via bash script) every 10min.
Both, the date and data line are written to a file (from stdout).
expect is hard to code but now it does what I need.
I have a C-program to parse the data from the script (written to a file) and to store them in a database.
I am not interested in historical data, only in recent data.
Could it be that there is a problem with gattlib ?
The C-code I am using for the old rd200 was modified taking into account the new UUIDs.
But this code just provides 0 after sending 0x50.
Anyhow, for me the problem is solved and the advantage of the new solution is not to care about UUIDs !

#!/usr/bin/expect -f
#
## remove output from STDOUT, except for puts
log_user 0
#
set timeout 4
#
spawn gatttool -b XX:XX:XX:XX:XX:XX -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 0x002a 50\r"

    set systemTime [clock seconds]
    puts "Time [clock format $systemTime -format %Y-%m-%dT%H:%M:%S]"
    set message1 ""

    expect {
             -re "Notification handle = 0x002c value: (.+\n)" {
              set message1 ${message1}$expect_out(1,string)
              exp_continue
        }
      }
      puts "$message1"
  }

send -- "exit\r"
#
expect eof

OK here’s the full radon_reader.py code with my attempted edits:

#!/usr/bin/python

""" 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"

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

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

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('-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)
parser.add_argument('-m','--mqtt',action='store_true',help='Also send radon value to a MQTT server', required=False)
parser.add_argument('-ms',dest='mqtt_srv',help='MQTT server URL or IP address', required=False)
parser.add_argument('-mp',dest='mqtt_port',help='MQTT server service port (Default: 1883)', required=False, default=1883)
parser.add_argument('-mu',dest='mqtt_user',help='MQTT server username', required=False)
parser.add_argument('-mw',dest='mqtt_pw',help='MQTT server password', required=False)
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)):
    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")
    RadonEye = btle.UUID("00001523-0000-1000-8000-00805f9b34fb")
    RadonEyeService = DevBT.getServiceByUUID(RadonEye)

    # 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")
    uuidWrite  = btle.UUID("00001524-0000-1000-8000-00805f9b34fb")
    RadonEyeWrite = RadonEyeService.getCharacteristics(uuidWrite)[0]
    RadonEyeWrite.write(bytes("\x50"))

    # 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")
    uuidRead  = btle.UUID("00001525-0000-1000-8000-00805f9b34fb")
    RadonEyeValue = RadonEyeService.getCharacteristics(uuidRead)[0]
    RadonValue = RadonEyeValue.read()
#    RadonValue = struct.unpack('<f',RadonValue[2:6])[0]
    RadonValue = struct.unpack('<H',RadonValue[2:4])[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 ):
        raise Exception("Very strange radon value. Debugging needed.")

    if args.becquerel:
        Unit="Bq/m^3"
#        RadonValue = ( RadonValue * 37 )
    else:
        Unit="pCi/L"
        RadonValue = ( RadonValue / 37 )
 
    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))
   
    if args.mqtt:
        if args.verbose and not args.silent:
            print ("Sending to MQTT...")
            if args.mqtt_ha:
                mqtt_out="Home Assistant"
            else:
                mqtt_out="EmonCMS"
            print ("MQTT Server: %s | Port: %s | Username: %s | Password: %s | Output: %s" % (args.mqtt_srv, args.mqtt_port, args.mqtt_user, args.mqtt_pw, mqtt_out))

        # REKey = Last 3 bluetooth address octets (Register/Identify multiple RadonEyes).
        # Sample: D7-21-A0
        REkey = args.address[9:].replace(":","-")

        clientMQTT = mqtt.Client("RadonEye_%s" % randint(1000,9999))
        clientMQTT.username_pw_set(args.mqtt_user,args.mqtt_pw)
        clientMQTT.connect(args.mqtt_srv, args.mqtt_port)

        if args.mqtt_ha:
            ha_var = json.dumps({"radonvalue": "%0.2f" % (RadonValue)})
            clientMQTT.publish("environment/RADONEYE/"+REkey,ha_var,qos=1)
        else:
            clientMQTT.publish("emon/RADONEYE/"+REkey,RadonValue,qos=1)

        if args.verbose and not args.silent:
            print ("OK")
        sleep(1)
        clientMQTT.disconnect()

try:
    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)

            sleep(5)
            GetRadonValue()

        except Exception as e:
            if args.verbose and not args.silent:
                print (e)

            if i < 3:
                continue
            else:
                print ("Failed.")
        break

Hopefully that will work.

I ran your expect script on my RD200, changing the MAC address to match my device. Here’s the result:

$ ./temp_expect.sh 
Time 2022-07-04T14:51:48
50 0a 38 00 3c 00 00 00 05 00 0a 00 

This works for me. The current value for my RD200 was 1.51 pCi/L, and this data matches that. The 3rd and 4th bytes (38 00), little-endian, contain the current value in Bq/m^3:
x0038 = 56 Bq/m^3 = 1.51 pCi/L.

Notice the change I made to radon_reader.py for getting the RadonValue from sending 0x50:

RadonValue = struct.unpack('<H',RadonValue[2:4])[0]

The response is apparently different from what the old RD200 used to send, since the was the old code:

RadonValue = struct.unpack('<f',RadonValue[2:6])[0]

They are now sending as short int (2-bytes) with Bq/m^3 instead of pCi/L as a 4-byte float.

Thanks for the post, same result for me unfortunately. I just get all 0x0’s back from the write/read.

I modified the script to work with Python3 and added a bunch of debugs and uploaded it to github:

Here is my debug output:

pi@hassio:~/radonreader $ python3 radon_reader2.py -a 94:3c:c6:dd:42:ce -v
2022-07-05 00:34:25,168 - root - DEBUG - Service <uuid=Generic Attribute handleStart=1 handleEnd=5>
2022-07-05 00:34:25,169 - root - DEBUG - Service <uuid=Generic Access handleStart=20 handleEnd=28>
2022-07-05 00:34:25,170 - root - DEBUG - Service <uuid=1523 handleStart=40 handleEnd=65535>
2022-07-05 00:34:25,170 - root - DEBUG - Reading: 00001523-0000-1000-8000-00805f9b34fb
2022-07-05 00:34:25,171 - root - DEBUG - RadonEyeService from UUID: Service <uuid=1523 handleStart=40 handleEnd=65535>
2022-07-05 00:34:25,171 - root - DEBUG - Writing UUID: 00001524-0000-1000-8000-00805f9b34fb
2022-07-05 00:34:25,509 - root - DEBUG - Service Characteristics: Characteristic <1524>
2022-07-05 00:34:25,509 - root - DEBUG - Writing Bytes: b'P'
2022-07-05 00:34:26,665 - root - DEBUG - Reading: 00001525-0000-1000-8000-00805f9b34fb
2022-07-05 00:34:26,714 - root - DEBUG - Radon Value: b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
2022-07-05 [00:34:26] - 94:3C:C6:DD:42:CE - Radon Value: 0.00 pCi/L

Update: I copied the methodology of using the handler reference in the ./temp_expect.sh script instead of using UUIDs and it actually works fine now. I will update the original radeon_reader.py with this code now…

pi@hassio:~/radonreader $ python3 radon_reader_by_handle.py
2022-07-05 01:51:27,215 - root - DEBUG - Radon Value Raw: b’P\n\x02\x00\x05\x00\x00\x00\x00\x00\x00\x00’
2022-07-05 01:51:27,215 - root - DEBUG - Radon Value Bq/m^3: 2
2022-07-05 01:51:27,215 - root - DEBUG - Radon Value pCi/L: 0.05405405405405406

Update #2
I updated radon_reader.py to:

-work with the new RD200
-auto scan for devices, so you don’t have to provide a MAC address
-automatically figure out the RD200 version based on the device name

Note:
– I tested the python app with my own RD200 v2, but do not have an RD200 v1 to test backwards comparability with.
– If you specify an address you now have to specify a device type -t (either 0 for the original RD200 or 1 for the new RD200)

1 Like

I was looking at gatttool, it can be run in non-interactive mode.
And one can request in which way UUIDs are connected to handles.

The old version of RD200:

gatttool -i hci0 -t random -b D5:04:05:65:AB:EF -I
[D5:04:05:65:AB:EF][LE]> connect
Attempting to connect to D5:04:05:65:AB:EF
Connection successful
[D5:04:05:65:AB:EF][LE]> primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0008, end grp handle: 0x0008 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0009, end grp handle: 0x0011 uuid: 00001523-1212-efde-1523-785feabcd123
attr handle: 0x0012, end grp handle: 0xffff uuid: 0000180a-0000-1000-8000-00805f9b34fb
[D5:04:05:65:AB:EF][LE]> char-desc
handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0004, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0006, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0007, uuid: 00002a04-0000-1000-8000-00805f9b34fb
handle: 0x0008, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0009, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x000a, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000b, uuid: 00001524-1212-efde-1523-785feabcd123
handle: 0x000c, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x000d, uuid: 00001525-1212-efde-1523-785feabcd123
handle: 0x000e, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x000f, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0010, uuid: 00001526-1212-efde-1523-785feabcd123
handle: 0x0011, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0012, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0013, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0014, uuid: 00002a29-0000-1000-8000-00805f9b34fb

gatttool -i hci0 -t random -b D5:04:05:65:AB:EF --char-write-req --handle=0x000b --value=0x50 --char-read --handle=0x000d
Characteristic value/descriptor: 50 10 00 00 00 00 5c 8f c2 3e c3 f5 a8 3e 00 00 01 00 00 00

The new version of RD200:

gatttool -b 1C:9D:C2:51:7F:5A -I
[1C:9D:C2:51:7F:5A][LE]> connect
Attempting to connect to 1C:9D:C2:51:7F:5A
Connection successful
[1C:9D:C2:51:7F:5A][LE]> primary
attr handle: 0x0001, end grp handle: 0x0005 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0014, end grp handle: 0x001c uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0028, end grp handle: 0xffff uuid: 00001523-0000-1000-8000-00805f9b34fb
[1C:9D:C2:51:7F:5A][LE]> char-desc
handle: 0x0001, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0002, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0003, uuid: 00002a05-0000-1000-8000-00805f9b34fb
handle: 0x0004, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x0014, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0015, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0016, uuid: 00002a00-0000-1000-8000-00805f9b34fb
handle: 0x0017, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x0018, uuid: 00002a01-0000-1000-8000-00805f9b34fb
handle: 0x0019, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x001a, uuid: 00002aa6-0000-1000-8000-00805f9b34fb
handle: 0x0028, uuid: 00002800-0000-1000-8000-00805f9b34fb
handle: 0x0029, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002a, uuid: 00001524-0000-1000-8000-00805f9b34fb
handle: 0x002b, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002c, uuid: 00001525-0000-1000-8000-00805f9b34fb
handle: 0x002d, uuid: 00002902-0000-1000-8000-00805f9b34fb
handle: 0x002e, uuid: 00002803-0000-1000-8000-00805f9b34fb
handle: 0x002f, uuid: 00001526-0000-1000-8000-00805f9b34fb
handle: 0x0030, uuid: 00002902-0000-1000-8000-00805f9b34fb

gatttool -i hci0 -b 1C:9D:C2:51:7F:5A --mtu=507 --char-write-req --handle=0x002a --value=0x50 --char-read --handle=0x002c
Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

The expect script is a “hack” which works. But certainly there is a problem with gattlib.

Seems a little strange. I think 0x002a resets to all 0’s after responding, and the read just gets the current, reset value.

Also:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=0x50 --listen
Characteristic value was written successfully
^C
$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=50 --listen
Characteristic value was written successfully
Notification handle = 0x002c value: 50 0a 30 00 3f 00 00 00 04 00 08 00 
^C

Seems like writing 0x50 doesn’t work, but writing 50 does, as long as --listen is used

The other problem is that non-interactive gatttool does not set MTU:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=41 --listen
Characteristic value was written successfully
Notification handle = 0x002f value: 41 04 01 fa 19 00 30 00 30 00 26 00 2e 00 2e 00 2d 00 23 00 
Notification handle = 0x002f value: 41 04 02 fa 26 00 26 00 10 00 1a 00 13 00 20 00 2b 00 31 00 
Notification handle = 0x002f value: 41 04 03 fa 31 00 3b 00 38 00 3e 00 30 00 44 00 31 00 2b 00 
Notification handle = 0x002f value: 41 04 04 79 27 00 2b 00 38 00 3b 00 41 00 48 00 44 00 35 00 
^C

The first three responses are each supposed to be 507 bytes total transmission (last one = 249), and this only returns 23 bytes (the default, minimum value).

For example, interactive:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A -I
[94:3C:C6:DE:31:7A][LE]> connect
Attempting to connect to 94:3C:C6:DE:31:7A
Connection successful
[94:3C:C6:DE:31:7A][LE]> mtu 507
MTU was exchanged successfully: 507
[94:3C:C6:DE:31:7A][LE]> char-write-req 0x002a 41
Characteristic value was written successfully
Notification handle = 0x002f value: 41 04 01 fa 19 00 30 00 30 00 26 00 2e 00 2e 00 2d 00 23 00 20 00 1d 00 2a 00 17 00 17 00 12 00 1d 00 1d 00 2b 00 34 00 2e 00 37 00 27 00 21 00 30 00 37 00 3a 00 2d 00 2b 00 1a 00 30 00 16 00 10 00 13 00 12 00 0f 00 17 00 16 00 24 00 19 00 16 00 26 00 1c 00 20 00 34 00 3f 00 26 00 35 00 3f 00 30 00 31 00 38 00 3f 00 44 00 38 00 26 00 19 00 24 00 2a 00 2e 00 21 00 20 00 1c 00 24 00 1c 00 26 00 38 00 2e 00 48 00 34 00 35 00 37 00 38 00 44 00 44 00 31 00 41 00 44 00 3b 00 27 00 2b 00 27 00 1d 00 17 00 2d 00 2b 00 27 00 26 00 2e 00 3b 00 44 00 44 00 3e 00 45 00 4e 00 37 00 34 00 44 00 2e 00 24 00 30 00 24 00 2b 00 2d 00 2a 00 30 00 1c 00 30 00 23 00 23 00 1c 00 23 00 31 00 20 00 3a 00 3e 00 3f 00 38 00 45 00 55 00 45 00 3a 00 42 00 2a 00 37 00 42 00 35 00 2e 00 38 00 2a 00 35 00 21 00 21 00 2a 00 26 00 24 00 3b 00 31 00 30 00 3e 00 30 00 3a 00 24 00 37 00 3b 00 37 00 26 00 41 00 30 00 1c 00 34 00 21 00 23 00 34 00 26 00 19 00 23 00 24 00 1d 00 19 00 16 00 2a 00 2e 00 34 00 3e 00 41 00 38 00 30 00 21 00 3a 00 2d 00 2a 00 2b 00 21 00 24 00 24 00 30 00 24 00 19 00 1c 00 16 00 1a 00 1a 00 21 00 27 00 2b 00 23 00 2b 00 27 00 2b 00 2a 00 3b 00 2d 00 2a 00 12 00 17 00 27 00 26 00 24 00 1d 00 27 00 20 00 1a 00 1d 00 20 00 23 00 24 00 31 00 20 00 2a 00 2a 00 31 00 34 00 3a 00 2d 00 42 00 34 00 35 00 34 00 3b 00 30 00 37 00 2e 00 27 00 26 00 17 00 2a 00 2b 00 20 00 19 00 26 00 2d 00 21 00 27 00 2e 00 38 00 37 00 38 00 30 00 27 00 21 00 31 00 24 00 26 00 19 00 2a 00 24 00 1a 00 1c 00 20 00 13 00 2a 00 
Notification handle = 0x002f value: 41 04 02 fa 26 00 26 00 10 00 1a 00 13 00 20 00 2b 00 31 00 3e 00 34 00 19 00 35 00 35 00 2e 00 30 00 37 00 24 00 2a 00 30 00 21 00 26 00 23 00 1c 00 19 00 1d 00 17 00 21 00 20 00 1d 00 2a 00 3b 00 24 00 20 00 26 00 24 00 30 00 2e 00 26 00 38 00 30 00 31 00 2a 00 2d 00 2b 00 2b 00 2b 00 26 00 2a 00 1d 00 24 00 1a 00 23 00 2e 00 30 00 45 00 41 00 2e 00 2b 00 34 00 38 00 3e 00 45 00 2d 00 42 00 35 00 2d 00 2a 00 26 00 1d 00 34 00 2e 00 20 00 26 00 2e 00 21 00 21 00 1c 00 2d 00 4e 00 41 00 45 00 38 00 31 00 3f 00 3a 00 38 00 35 00 23 00 26 00 27 00 24 00 16 00 2d 00 30 00 1c 00 2a 00 10 00 27 00 20 00 20 00 1d 00 26 00 2d 00 31 00 37 00 3e 00 31 00 2e 00 3a 00 2d 00 49 00 4b 00 41 00 37 00 2d 00 3f 00 3b 00 30 00 26 00 31 00 27 00 41 00 30 00 3b 00 41 00 5c 00 5a 00 45 00 44 00 4b 00 49 00 4c 00 45 00 30 00 48 00 45 00 41 00 38 00 2b 00 3a 00 31 00 37 00 24 00 27 00 26 00 17 00 31 00 34 00 23 00 2b 00 45 00 34 00 31 00 35 00 31 00 35 00 30 00 3e 00 42 00 2e 00 3a 00 2e 00 20 00 2a 00 23 00 1d 00 13 00 2e 00 16 00 1d 00 26 00 2a 00 27 00 31 00 37 00 37 00 3f 00 31 00 50 00 37 00 37 00 34 00 26 00 41 00 20 00 2b 00 23 00 2d 00 27 00 2d 00 34 00 2a 00 2b 00 35 00 2a 00 3b 00 42 00 49 00 45 00 50 00 50 00 3e 00 42 00 3a 00 4c 00 52 00 3f 00 41 00 42 00 3a 00 37 00 48 00 31 00 3e 00 30 00 3e 00 3e 00 3f 00 3a 00 38 00 2e 00 45 00 60 00 45 00 63 00 45 00 59 00 4c 00 38 00 52 00 42 00 3b 00 3a 00 35 00 3e 00 2a 00 30 00 26 00 24 00 2e 00 26 00 16 00 20 00 20 00 16 00 24 00 2d 00 35 00 34 00 23 00 
Notification handle = 0x002f value: 41 04 03 fa 31 00 3b 00 38 00 3e 00 30 00 44 00 31 00 2b 00 2a 00 37 00 1c 00 31 00 20 00 1d 00 30 00 2a 00 27 00 3a 00 2a 00 30 00 48 00 48 00 44 00 37 00 2d 00 2e 00 35 00 4b 00 3b 00 31 00 35 00 3e 00 1d 00 27 00 2e 00 2d 00 2d 00 2a 00 30 00 20 00 20 00 37 00 35 00 49 00 56 00 4c 00 3a 00 2b 00 3e 00 3a 00 3e 00 4c 00 37 00 30 00 38 00 2a 00 30 00 2d 00 2e 00 1d 00 2b 00 3a 00 3b 00 27 00 49 00 50 00 3b 00 41 00 42 00 5d 00 4c 00 3b 00 3a 00 23 00 30 00 31 00 3a 00 49 00 26 00 2a 00 26 00 2d 00 20 00 20 00 20 00 26 00 1d 00 24 00 1a 00 0f 00 2d 00 23 00 24 00 23 00 26 00 1d 00 34 00 35 00 31 00 34 00 23 00 2a 00 2a 00 26 00 27 00 1d 00 26 00 35 00 30 00 2e 00 13 00 35 00 34 00 24 00 2a 00 26 00 3a 00 34 00 3e 00 3b 00 2d 00 3e 00 44 00 3a 00 2b 00 3b 00 31 00 23 00 35 00 2e 00 30 00 2d 00 30 00 26 00 45 00 3e 00 3f 00 38 00 38 00 37 00 30 00 24 00 2b 00 42 00 41 00 3f 00 3f 00 3b 00 30 00 30 00 3f 00 30 00 35 00 13 00 1a 00 20 00 27 00 3b 00 2d 00 35 00 38 00 41 00 38 00 42 00 3f 00 31 00 3e 00 2e 00 30 00 4e 00 3b 00 38 00 31 00 3a 00 31 00 38 00 2e 00 2b 00 23 00 37 00 2a 00 19 00 2b 00 31 00 2a 00 31 00 52 00 66 00 52 00 5c 00 49 00 4c 00 42 00 52 00 45 00 37 00 37 00 3f 00 4b 00 23 00 3b 00 30 00 27 00 2a 00 2a 00 2d 00 37 00 2a 00 20 00 2a 00 31 00 3b 00 31 00 30 00 31 00 4c 00 3f 00 5f 00 41 00 3b 00 31 00 42 00 3e 00 3a 00 2d 00 24 00 34 00 2d 00 2e 00 21 00 31 00 2e 00 26 00 2d 00 26 00 56 00 3e 00 41 00 3b 00 4b 00 56 00 45 00 4c 00 3f 00 34 00 45 00 3e 00 42 00 42 00 30 00 
Notification handle = 0x002f value: 41 04 04 79 27 00 2b 00 38 00 3b 00 41 00 48 00 44 00 35 00 38 00 4e 00 3b 00 3e 00 3e 00 4b 00 71 00 42 00 56 00 52 00 4c 00 37 00 31 00 34 00 30 00 20 00 19 00 24 00 1d 00 2a 00 19 00 24 00 2d 00 1a 00 31 00 2b 00 37 00 34 00 3a 00 41 00 4c 00 38 00 3f 00 3b 00 2d 00 37 00 3f 00 35 00 27 00 2a 00 23 00 2a 00 23 00 2a 00 37 00 3f 00 38 00 4e 00 3e 00 44 00 48 00 49 00 38 00 41 00 38 00 45 00 50 00 41 00 38 00 34 00 3a 00 3f 00 30 00 44 00 21 00 37 00 2d 00 21 00 19 00 3b 00 30 00 2d 00 4e 00 4b 00 59 00 55 00 44 00 3a 00 52 00 50 00 66 00 5d 00 31 00 4c 00 42 00 42 00 2e 00 30 00 2d 00 21 00 24 00 31 00 2a 00 24 00 37 00 24 00 2e 00 42 00 49 00 5c 00 64 00 5a 00 50 00 42 00 48 00 48 00 38 00 41 00 41 00 31 00 52 00 2d 00 31 00 
[94:3C:C6:DE:31:7A][LE]> disconnect

(gatttool:2066): GLib-WARNING **: 16:04:05.144: Invalid file descriptor.

[94:3C:C6:DE:31:7A][LE]> exit

Definitely not getting all of the bytes using non-inteactive. Also, trying to read that value (0x002f) instad of --listen with non-interactive also fails:

$ gatttool -i hci0 -b 94:3C:C6:DE:31:7A --mtu=507 --char-write-req --handle=0x002a --value=41 --char-read --handle 0x002f
Characteristic value/descriptor: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

I am using a pretty hacked together python3 script that uses pygatt, with a gatttool backend. It works fine, other than its ugliness and lack of error trapping:

#! /usr/bin/env python3

import pygatt
import time
from struct import unpack
import csv
from datetime import timedelta
from datetime import datetime
import os
from influxdb import InfluxDBClient

## This changes directory to the location of the script when called by crontab (on external storage device):
abspath = os.path.abspath(__file__)
dirname = os.path.dirname(abspath)
os.chdir(dirname)

## Change this to RD200 MAC:
radon_mac = 'XX:XX:XX:XX:XX:XX'

## influx configuration - edit these:
ifuser = "username"
ifpass = "password"
ifdb   = "dbname"
ifhost = "127.0.0.1"
ifport = 8086
measurement_name = "Radon"

## Use GATTTool backend on RPi:
adapter = pygatt.GATTToolBackend()

complete = False
history = []
## Get current time:
now = datetime.now()

## Round to the nearest 5 minutes (not required):
now = now - timedelta(minutes=now.minute % 5,
                      seconds=now.second,
                      microseconds=now.microsecond)

## This manages history data and writes to CSV
## Eventually I want to clear/recreate InfluxDB table with new info
## handle_data gets the history values using 0x41:
def handle_data(handle, value):
#    print("Received data: %s" % value.hex())
    
    ## Using global lets me tell main program that this is finished
    global complete
    global history
    req_no = value.pop(0)
    tot_resps = value.pop(0)
    this_resp = value.pop(0)
    num_values = value.pop(0)
    ## After removing the first 4 bytes, the rest contain history in Bq/m^3:
    Bq_m3 = unpack('<'+'H'*(len(value)//2),value)
#    print(len(Bq_m3))
    Bq_m3 = list(Bq_m3)
    pCi_L = [x/37 for x in Bq_m3]
    resp_len = len(Bq_m3)
    if num_values != resp_len:
        print("Number of values didn't match!")

#    print(f"Requested {req_no}, got {tot_resps} replies, this is reply {this_resp} containing {num_values} values.")
#    print(f"Values: {value.hex()}")
#    print(f"First 4 values out of {resp_len} total in this response: {Bq_m3[0:4]} Bq/m^3")
#    print(f"First 4 values out of {resp_len} total in this response: {pCi_L[0:4]} pCi/L")

    ## This adds the latest values to history:
    history.extend(pCi_L)

    ## If this response is the last response, set complete to True:
    if this_resp == tot_resps:
        #print("Completed")
        complete = True


## handle_current gets the current data using 0x40:
def handle_current(handle, value):
    currentTime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    ## take a timestamp for this measurement
    time = datetime.utcnow()

    #print("Received data: %s" % value.hex())
    
    ## global complete lets the main program know this is finished:
    global complete
    req_no = value.pop(0) # first byte is 0x40 from write
    extra_no = value.pop() # don't know what last byte is for... removed it
    
    Bq_m3 = unpack('<'+'H'*(len(value)//2),value)
#    print(len(Bq_m3))
    resp_len = len(Bq_m3)
#    print(f"Values: {value.hex()}")
#    print(Bq_m3)
    latest = Bq_m3[16]/37 # the latest value is stored at 16
    peak = Bq_m3[25]/37 # the peak value is stored at 25
#    print(currentTime)
    print(f"Latest value = {latest:.2f} pCi/L at {currentTime}")
    print(f"Peak value = {peak:.2f} pCi/L")
    
    ## Append to CSV file using rounded time (now):
    with open("RadonCurrentValues.csv", "a") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow([now,latest,peak])

    ## format the data as a single measurement for influx
    body = [
        {
        "measurement": measurement_name,
        "time": time,
        "fields": {
            "radon": latest,
            "peak": peak
            }
        }
    ]
    ## connect to influx
    ifclient = InfluxDBClient(ifhost,ifport,ifuser,ifpass,ifdb)
    ## write the measurement
    ifclient.write_points(body)

    complete = True

try:
    adapter.start()

    try:
        device = adapter.connect(radon_mac,timeout=20)
    except:
        print("Couldn't connect, retrying...")
        device = adapter.connect(radon_mac,timeout=20)

    ## Set mtu to get the full history responses:
    try:
        device.exchange_mtu(507)
    except:
        print("Couldn't set MTU, retrying...")
        device.exchange_mtu(507)

    
    print("Subscribing to history response...")
    
    ## For some reason there is no subscribe_handle, even though char_write_handle exists... weird.
    #device.subscribe_handle(0x002f,
    #                 callback=handle_data,
    #                 indication=False,
    #                 wait_for_response=False)

    ## Subscribe to UUID:
    device.subscribe("00001526-0000-1000-8000-00805f9b34fb",
                     callback=handle_data,
                     indication=False,
                     wait_for_response=False)

    ## Request history data by sending 0x41:
    print("Requesting data...")
    device.char_write_handle(0x002a,
                      bytearray([0x41]),
                      wait_for_response=False)

    ## Could also subscribe to UUID instead of handle:
    #device.char_write("00001524-0000-1000-8000-00805f9b34fb",
    #                  bytearray([0x41]),
    #                  wait_for_response=False)

    while not (complete) :
        time.sleep(1) # wait one second until handle_data sets complete to True
#        print(f"Complete = {complete}")

    device.unsubscribe("00001526-0000-1000-8000-00805f9b34fb")

    ## This doesn't store dates for history, just one per hour
    ## Cound history and subtract 1 hour each (assuming device was kept running continuously):
    historyCount = len(history)
    difference = timedelta(hours=1)
    times = [now - x*difference for x in list(range(historyCount-1, -1, -1))]
    
    ## Write history values to CSV file or DB here (new file each time):
    with open("RadonHistory.csv", "w") as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["Timestamp","Radon (pCi/L)"])
        for value in range(len(history)):
            writer.writerow([times[value], history[value]])


    ## Now, set complete to False again to get current values:
    complete = False

    ## Make sure file exists, otherwise write header:
    if not os.path.isfile("RadonCurrentValues.csv"):
        with open("RadonCurrentValues.csv", "w") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(["Timestamp","Radon (pCi/L)","Peak value (pCi/L)"])


    print("Subscribing to current response...")
    
    ## Subscribe to 1525 for current data:
    device.subscribe("00001525-0000-1000-8000-00805f9b34fb",
                     callback=handle_current,
                     indication=False,
                     wait_for_response=False)
    
    print("Requesting data...")

    ## Request current data by sending 0x40:
    device.char_write_handle(0x002a,
                      bytearray([0x40]),
                      wait_for_response=False)

    while not (complete) :
        time.sleep(1) # wait one second until handle_data sets complete to True
#        print(f"Complete = {complete}")

    #device.unsubscribe("00001525-0000-1000-8000-00805f9b34fb")
    ## Why is it not subscribed? Leave it out...
    ## For some reason that caused an error

finally:
    adapter.stop()
#    print(f"Historical data (Bq/m^3): {history}")
    print(f"Total hours returned for history: {len(history)}")

Pygatt required me to subscribe to the response to get the correct data. I don’t know how that relates to using non-interactive gatttool, but just reading from the handle would return 0’s unless it was a response to the request.

Hey, thanks for the handle for the old device. I updated the file in the repo to do either 0x002a for the new device and 0x000b for the old device.

Could someone test the radon_reader.py in my github with an old RD200 device? It is 100% working with the my new RD200 now.

I also added instructions in the README on how to integrate the MQTT messages into Home Assistant.

Maybe that was a typo:

Old device:
handle: 0x000d, uuid: 00001525-1212-efde-1523-785feabcd123

The handles are:

New device:
send 0x002a → notification receive on: 0x002c

Old device:
send 0x000b → notification receive on: 0x000d

I updated the ./temp_expect.sh expect script that was posted earlier to account for both device types. It is also working on my new RD200.

Strange. With the old devices only the non-interactive command works:

gatttool -i hci0 -t random -b D5:04:05:65:AB:EF --char-write-req --handle=0x000b --value=50 --char-read --handle=0x000d
Characteristic value/descriptor: 50 10 ec 51 b8 3d 85 eb d1 3e c3 f5 a8 3e 00 00 01 00 00 00