Skip to content

Instantly share code, notes, and snippets.

@nickovs
Created May 28, 2022 19:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nickovs/f2e20f0352b146eb1daf5c40ef25be35 to your computer and use it in GitHub Desktop.
Save nickovs/f2e20f0352b146eb1daf5c40ef25be35 to your computer and use it in GitHub Desktop.
A simple script for receiving water meter readings from a Badger ORION water meter using an Software Defined Radio (SDR).
#!/usr/bin/env python3
# A simply tool for receiving water meter readings from a Badger ORION water meter.
# Device radio details can be found at https://fccid.io/GIF2006B
# Requires rtl_433 to be installed. See https://github.com/merbanan/rtl_433
import sys
import json
import subprocess
RTL_PATH = "/usr/local/bin/rtl_433"
nibble_codes = [0x16, 0x0D, 0x0E, 0x0B, 0x1c, 0x19, 0x1A, 0x13, 0x2C, 0x25, 0x26, 0x23, 0x34, 0x31, 0x32, 0x29]
nibble_map = {code: i for i, code in enumerate(nibble_codes)}
def crc16dnp(data):
poly = 0x13d65
crc = 0
for d in data:
crc = crc << 8 | d
for i in range(7, -1, -1):
crc ^= (poly << i) * ((crc >> 16 + i) & 1)
return crc
def decode_byte(digits):
# The 4:6 encoding means that one byte is encoded in three hex digits
raw = int(digits, 16)
try:
return (nibble_map[raw >> 6] << 4) + nibble_map[raw & 0x3f]
except KeyError as kerr:
raise ValueError("Unknown encoding symbol") from kerr
def decode_record(r):
timestamp = int(r['time'])
rssi = r["rssi"]
data_length = r['rows'][0]['len']
if data_length < 120:
raise ValueError("Short packet")
data_hex = r['rows'][0]['data'][:30]
packet = bytearray(decode_byte(data_hex[i * 3:(i + 1) * 3]) for i in range(10))
if crc16dnp(packet) != 0xffff:
raise ValueError("Bad checksum")
device_id = int.from_bytes(packet[:4], "little")
reading = int.from_bytes(packet[4:7], "little")
return {"timestamp": timestamp, "device_id": device_id, "reading": reading, "rssi": rssi}
protocol_spec = {
"name": "badger",
"modulation": "FSK_PCM",
"short": 10,
"long": 10,
"reset": 1000,
"preamble": "{16}543d" # 6 bits of sync (010101), 10 bits pre-amble (0000111101)
}
frequency = 916450000
sample_rate = 1000000
command = [
RTL_PATH,
"-f", str(frequency),
"-s", str(sample_rate),
"-R", "0",
"-X", ",".join(f"{key}={value}" for key, value in protocol_spec.items()),
"-F", "json",
"-M", "time:unix",
"-M", "level",
]
def main_loop():
print("Starting radio:", " ".join(command), file=sys.stderr)
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
try:
for line in proc.stdout:
try:
raw_record = json.loads(line.strip())
except json.JSONDecodeError:
print("Bad JSON", file=sys.stderr)
continue
try:
record = decode_record(raw_record)
except ValueError as e:
print("Bad data:", e, file=sys.stderr)
continue
print(record)
except KeyboardInterrupt:
print("Exiting", file=sys.stderr)
finally:
proc.kill()
proc.wait()
print("Done.", file=sys.stderr)
if __name__ == "__main__":
main_loop()
@jaydeethree
Copy link

Thanks so much for this! Do you know how often the data from the meter is updated? I've seen discussions that some Badger Orion meters only update their data once per hour, which wouldn't be often enough for what I'm trying to do. I have the same meter as you (FCC ID GIF2006B) and if the data is updated at least every 5 minutes then this would work great for my project, but if it doesn't update that often then I'll need to look at other options like building a custom pulse meter. Thanks!

@nickovs
Copy link
Author

nickovs commented Oct 5, 2023

The reading is retransmitted every few seconds and the value transmitted is taken live from the mechanical meter, so you can get very timely updates. If I flush the toilet I can see the water consumption from the refilling of the cistern in the next few readings!

As far as I can tell there are different models of mechanical meter that use the same transmitter. If you have a very large property with a large water main then the reading resolution may use a unit 10 times larger. I suspect that the exact interval between transmissions varies in order to avoid interference when there are multiple meters nearby, but I would expect the interval to always be of the order of 5 seconds. The meters are designed to be read using a car-mounted unit that you drive down the street; having to wait for minutes outside each house would not be efficient!

@nickovs
Copy link
Author

nickovs commented Oct 5, 2023

@jaydeethree Also, since I wrote the script above, I also submitted a PR for rtl_433 to include protocol decoding for the Orion Badger water meters as standard. As such, if you have a recent (post August 2022) version of rtl_433, you don't need to do any protocol decoding and you can capture the messages directly using something like:

rtl_433 -f 916450000 -s 1600000 -R 219

Here -R 219 selects protocol index 219, which is the Badger protocol. You can then play with -F and -M to get the most useful format and metadata for each record. For data logging I would suggest something like -F json -M time:unix:usec:utc.

@jaydeethree
Copy link

Thanks so much for the quick and detailed response, I really appreciate it! I'll pick up an SDR dongle and try this out :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment