Python implementation for reading an Itron EverBlu Cyble Enhanced V2.1 water
meter over 433 MHz using a Raspberry Pi 5 and a CC1101 SPI
transceiver. Ports the field-tested C project
neutrinus/everblu-meters to
native Python 3 and adds a wiring/health diagnostic suite.
The meter uses the proprietary Radian protocol (2-FSK on 433.82 MHz). It only wakes to listen on weekdays, typically 06:00–18:00. Outside the window the meter will not respond — this is expected, not a bug.
Yes, my meter is dirty!
| Pi header | BCM | CC1101 pin |
|---|---|---|
| Pin 17 | 3V3 | VCC |
| Pin 20 | GND | GND |
| Pin 11 | GPIO17 | GDO0 |
| Pin 13 | GPIO27 | GDO2 |
| Pin 19 | MOSI | MOSI |
| Pin 21 | MISO | MISO |
| Pin 23 | SCLK | SCK |
| Pin 24 | CE0 | CSN |
Enable SPI on the Pi: sudo raspi-config → Interface Options → SPI → enable. Reboot.
Run the following to clone to your raspberry pi:
git clone https://github.com/jonnyry/everblu-cyble-reader-rpi-python.gitRun the following to setup your python environment, and grant access to SPI and GPIO:
cd everblu-cyble-reader-rpi-python
./install.shinstall.sh does the following:
- Enables SPI via raspi-config
- Install system packages via apt
- Grant current user access to SPI and GPIO
- Create a Python virtual environment
- Install Python packages
You must log out and back in (or reboot) for the spi/gpio group membership to take effect.
Verify the board is wired correctly and the CC1101 is alive before trying to talk to the meter:
./run.sh diagExample output:
[PASS] CC1101 PARTNUM/VERSION: PARTNUM=0x00 VERSION=0x14
[PASS] PATABLE write/read-back: wrote=['0x11', '0x22', '0x33', '0x44', '0x55', '0x66', '0x77', '0x88'] read=['0x11', '0x22', '0x33', '0x44', '0x55', '0x66', '0x77', '0x88']
[PASS] Strobe & state transitions: IDLE=0x01 RX=0x0D IDLE2=0x01
[PASS] Frequency register programming: wrote 433820000 Hz, read back 433819855 Hz (err 145 Hz)
[PASS] GDO0/GDO2 wiring: GDO0(pin17): low=0 high=1; GDO2(pin27): low=0 high=1
[PASS] XOSC/192 clock on GDO0: 2690 edges in 10.0 ms (observed ~134.5 kHz, true 135.4 kHz; polling can undersample)
[PASS] RX noise floor (RSSI): min=-131.5 avg=-102.3 max=-99.0 dBm over 50 samples
[PASS] Config register dump: 47 registers: 0D 2E 06 47 55 00 FF 00 00 00 00 08 00 10 AF 75 F6 83 02 00 00 15 07 00 18 1D 1C C7 00 B2 87 6B FB B6 10 E9 2A 00 1F 41 00 59 7F 3F 81 35 09
If you get any fails, recheck the wiring and try again. Please note I noticed fails when using jumper wires > 10cms long.
To read the meter, provide the first two components of serial number of RF unit (eg 15-0202517-123) to the --year and --serial parameters as follows:
./run.sh read_meter --year 15 --serial 202517 --jsonExample output with the --json flag:
{
"meter_id": "55SF123456",
"timestamp": "2026-04-24T09:49:23.587257+0100",
"liters": 1996330,
"reads_counter": 44,
"battery_months": 9,
"window_start_hour": 6,
"window_end_hour": 18
}Please note: the timestamp is taken from the Raspberry Pi and is NOT returned in the meter payload.
To append to a readings log file:
./run.sh read_meter --year 15 --serial 202517 --json > readings.log| Option | Description |
|---|---|
--year |
First segment of the label serial (15-0202517-123 → 15) |
--serial |
Middle segment of the label serial with leading zeros stripped (e.g. 15-0202517-123 → 202517). |
--freq-offset-hz |
Frequency trim (Hz) added to 433.82 MHz; only needed if not using default -15000 calibration. |
--retries 3 |
Retry count; the meter may miss the first wake-up |
--retry-delay 5 |
Seconds between retries |
--json |
Machine-readable output |
--raw |
Include the raw hex frame |
--verbose |
Include debug frames (request payload, RX phase info) |
--additional-readings |
Include the list of additional readings (unspecified/unclear purpose) |
Once you have a several readings in JSON format in a log file (see automation section below for generating a log file), generate an HTML dashboard with SVG bar charts:
./run.sh chart --log-file automation/readings.log --output-dir ~/www| Option | Description |
|---|---|
--log-file |
Path to the readings log produced by read_meter --json |
--output-dir |
Directory to write output files (default: current directory) |
This produces a dashboard like the following:
The dashboard shows:
- A bar chart of litres used per day over the last 7 days
- A bar chart of litres used per day over the last 30 days
- A table of the last 7 actual meter readings with timestamps and cumulative value in m³
automation/cron_read_meter.sh takes a reading, appends it to a log file, regenerates the charts, and copies them to ~/www — all in one step, designed to be run from cron.
Copy the sample and fill in your meter details:
cd automation
cp meter_config.env.sample meter_config.envEdit automation/meter_config.env:
YEAR="15" # first segment of the label serial (e.g. 15-0202517-123 → 15)
SERIAL="202517" # middle segment with leading zeros strippedbash automation/cron_read_meter.shOn success, automation/readings.log will gain a new JSON entry and ~/www/ will contain the updated dashboard. Errors are written to automation/errors.log.
The meter only listens on weekdays between 06:00–18:00, so schedule within that window. Once per day is sufficient:
crontab -eAdd a line such as:
0 7 * * 1-5 /home/pi/everblu-cyble-reader-rpi-python/automation/cron_read_meter.sh
This runs at 07:00 Monday–Friday. Adjust the path to match your installation.
The script respects these environment variables if you need to change the defaults:
| Variable | Default | Description |
|---|---|---|
LOG_FILE |
automation/readings.log |
Successful readings log |
ERROR_LOG |
automation/errors.log |
Error output log |
CHART_OUT |
automation/chart_out |
Intermediate chart files |
WWW_DIR |
~/www |
Published dashboard destination |
CC1101 modules ship with crystals that drift a few kHz from nominal; the
meter's narrow RX filter will drop the request unless you trim for it. On
this installation the sweep picked up the meter at -20 kHz first try, with
a reliable band from -30 kHz to -10 kHz; -15 kHz is now the default.
To recalibrate for a different module:
# 1. Coarse sweep (±80 kHz in 20 kHz steps):
./run.sh freq_scan --year 15 --serial 202517 --start-hz -80000 --stop-hz 80000 --step-hz 20000
# 2. Narrow in on the hit with a finer step:
./run.sh freq_scan --year 15 --serial 202517 --start-hz -30000 --stop-hz -10000 --step-hz 2500Pick the midpoint of the reliable band and either pass it as
--freq-offset-hz <value> to read_meter.py or update the freq_offset_hz
default in everblu/config.py.
Hardware-independent unit tests:
./run.sh unit_testeverblu/
config.py Meter / radio / GPIO configuration dataclasses
cc1101_regs.py CC1101 register & strobe constants, default config table
cc1101.py Thin spidev-based CC1101 driver
gpio.py lgpio-based GPIO wrapper (Pi 5 compatible)
radian.py Pure-Python Radian protocol (CRC, encode, decode, parse)
reader.py Wake-up + request + 2-stage RX orchestration
diagnostics.py Wiring / chip health checks
scripts/
diag.py CLI: run diagnostics
read_meter.py CLI: read the meter
freq_scan.py CLI: sweep frequency to find the crystal calibration
water_chart.py CLI: generate HTML dashboard and SVG charts from log
tests/
test_radian.py CRC, encode/decode and frame construction tests
test_cc1101.py Driver tests against an in-memory SPI mock
See comments in everblu/radian.py for the encoding details. Summary:
- TX: ~2 s wake-up (
0x55bytes at 2.4 kbps, no preamble/sync), followed by a 39-byte frame = 9-byte fixed sync pattern + 19-byte CRC-Kermit-protected payload carrying the meter year + 24-bit serial. The 19-byte payload is async-serial encoded: each byte prepended with a 0 start bit, followed by three 1 stop bits, transmitted LSB-first. - RX: two-stage sync capture. First SYNC
0x5550at 2.4 kbps to grab the short ACK; then re-sync on0xFFF0at 9.59 kbps for the long index frame withPKTCTRL0=0x02(infinite length). - Decode: the CC1101 captures at 4× the symbol rate, so
decode_4bitpbitcollapses 4-sample runs and strips the start/stop framing, yielding the raw payload bytes. Litres are a 32-bit little-endian integer at offset 18; seeparse_meter_reportfor other documented fields.
Derived from neutrinus/everblu-meters
(C/wiringPi) and the
psykokwak-com/everblu-meters-esp8266
fork, which in turn reverse-engineered the Radian protocol from
http://www.lamaisonsimon.fr/wiki/doku.php?id=maison2:compteur_d_eau:compteur_d_eau.
The license is unknown, citing one of the authors:
I didn't put a license on this code maybe I should, I didn't know much about it in terms of licensing. this code was made by "looking" at the radian protocol which is said to be open source earlier in the page, I don't know if that helps?



