Se rendre au contenu

Build a Raspberry Pi Energy Monitor with rbAmp — Live Watts in Python, No Cloud

Read real mains power on a Raspberry Pi in a few lines of Python — voltage, current, watts and kilowatt-hours from an rbAmp module over I²C, printed live to the terminal. No Wi-Fi service, no cloud, the on-ramp to the rbAmp Python library.
30 juillet 2026 par
Build a Raspberry Pi Energy Monitor with rbAmp — Live Watts in Python, No Cloud
Administrator

If your home server is a Raspberry Pi, you don't need an Arduino or Home Assistant to read your mains — a few lines of Python and an rbAmp module will print 230.4 V · 0.21 A · 48 W · 0.97 PF to the terminal, updating every second, and keep a running kilowatt-hour total. No cloud, no broker, no service. It's also the gentlest on-ramp to the rbAmp Python library; everything here carries into the bigger Pi projects (multi-circuit metering, load control, data logging) later in this series.

What you'll build

  • Live voltage, current, real power, and power factor printed to the terminal, about once a second.
  • A running energy (Wh) total, integrated on the Pi over wall-clock time.
  • All over the Pi's I²C bus — four wires — in plain CPython.
  • No Wi-Fi service, no cloud, no HA. Just a script you run.

Bill of materials

Item Qty Notes
rbAmp Basic Wattmeter (UI1) 1 voltage + current + on-chip real power · product
SCT-013 current transformer 1 rating to match your circuit · product
Raspberry Pi 1 any model with the 40-pin I²C header
Jumper wires the I²C bus (SDA / SCL / 5V / GND)

Enable I²C and install the library

On the Pi, turn on the I²C interface once — sudo raspi-configInterface OptionsI2C → enable (or add dtparam=i2c_arm=on to /boot/firmware/config.txt and reboot). Then install the package:

bash
pip install rbamp smbus2

(The Pi's kernel I²C driver runs at 100 kHz by default and is happy with the module — there's no 50 kHz workaround here; that only applies to the ESP-IDF driver on ESP32.)

Wire it up

The rbAmp module goes on the Pi's I²C pins:

plaintext
Raspberry Pi header          rbAmp (0x50)
-------------------          ------------
pin 3  SDA (GPIO2) ────────── SDA
pin 5  SCL (GPIO3) ────────── SCL
pin 2  5V          ────────── VCC        (rbAmp wants 5 V)
pin 6  GND         ────────── GND
                              │
                              └── SCT-013 clamped around ONE mains conductor (arrow → load)
  • The Pi's I²C bus is 3.3 V logic, and the rbAmp module has on-board pull-ups — so no extra pull-ups for a single module. Power the module at 5 V (the Pi's 5 V pins); the bus lines idle at 3.3 V. (Why 5 V power but a 3.3 V bus?)
  • The SCT-013 clamps around an insulated conductor — you never cut or strip live wire for the current side. The AC voltage side connects to mains; if you're not comfortable near live conductors, have a qualified person make that connection. Mains is dangerous.

The script

Save it, set MY_CT (or pass --ct) to match your clamp, and run python tp1_standalone_pi_monitor.py:

python
"""
T-P1 — Standalone Raspberry Pi energy monitor (no network, no HA).
One rbAmp UI1 module on the Raspberry Pi hardware I2C bus. Prints live
mains Voltage / Current / Power / Power Factor to the terminal every
second, and every 30 s latches a period snapshot to roll a host-side
Energy (Wh) total. Nothing is invented on-device: Wh is integrated on
the host from average power x wall-clock dt, which is what makes the
total honest.
Wiring (Raspberry Pi, hardware I2C):
    rbAmp SDA -> GPIO2  (Pi pin 3)
    rbAmp SCL -> GPIO3  (Pi pin 5)
    rbAmp GND -> any GND pin
    rbAmp VCC -> 5V     (Pi pin 2 or 4; 4.5-5.5 V -- below 4.5 V the ADC loses accuracy)
Enable I2C once on the Pi:
    sudo raspi-config    # Interface Options -> I2C -> Enable
                         # (equivalent to `dtparam=i2c_arm=on`)
The stock Raspberry Pi Broadcom I2C driver runs at 100 kHz and is
clean with rbAmp straight out of the box -- no `dtparam=i2c_arm_baudrate`
override is needed. (The 50 kHz workaround only applies to the ESP-IDF
i2c_master driver on ESP32.)
Install:
    pip install rbamp smbus2
Run:
    python tp1_standalone_pi_monitor.py
    # or with a non-default bus / address / CT model:
    #   python tp1_standalone_pi_monitor.py --bus 1 --addr 0x50 --ct 3
Set MY_CT below (or pass --ct) to match YOUR clamp before first run.
The value persists to the module's flash, so this script only re-writes
it when it actually differs -- no flash wear on every power-up.
Accuracy: +/-0.5% of reading per channel with the matched rbAmp CT.
Ctrl-C to stop.
"""
import argparse
import time
from smbus2 import SMBus
from rbamp import (
    RbAmp,
    RbAmpError,
    RbAmpStaleError,
    RbAmpSensorClass,
)
# ---- configuration --------------------------------------------------------
# CT model code (matches your clamp; persists to module flash):
#   1 = SCT-013-005 (5 A)     4 = SCT-013-050 (50 A)
#   2 = SCT-013-010 (10 A)    6 = SCT-013-020 (20 A)
#   3 = SCT-013-030 (30 A)
MY_CT = 3
LIVE_S   = 1.0    # refresh V/I/P/PF each second
ENERGY_S = 30.0   # latch a period + roll Wh every 30 s
def ensure_ct_preset(dev, ct_code):
    """Verify-then-set: only rewrite the CT preset if the module isn't
    already on it. set_sensor_class / set_ct_model_ch both persist to
    flash, so this avoids a needless flash write every power-up.
    """
    try:
        applied = dev.read_ct_model_ch(0)
    except RbAmpError:
        applied = -1
    if applied == ct_code:
        print(f"CT model already SCT_013 code={ct_code}; skipping flash write")
        return False
    print(f"configuring CT model SCT_013 code={ct_code} (persists to flash)...")
    dev.set_sensor_class(RbAmpSensorClass.SCT_013)
    dev.set_ct_model_ch(0, ct_code)
    return True
def read_live(dev, last, counters):
    """Read one live sample. A single transient hiccup keeps the previous
    plausible reading so the terminal keeps ticking (mirrors the Arduino
    T-A1 NaN-guard)."""
    try:
        v  = dev.voltage
        i  = dev.current[0]
        p  = dev.power[0]
        pf = dev.power_factor[0]
    except RbAmpError as e:
        counters["bad"] += 1
        print(f"read skipped: {type(e).__name__} -- {e}")
        return
    last["V"], last["I"], last["P"], last["PF"] = v, i, p, pf
    counters["ok"] += 1
    print(
        f"U={v:6.1f}V  I={i:6.3f}A  P={p:7.1f}W  PF={pf:+.2f}  "
        f"E={dev.energy.wh(0):8.4f}Wh  "
        f"[ok={counters['ok']} bad={counters['bad']}]"
    )
def roll_energy(dev):
    """Latch a metering period; the library rolls Wh from avg power x master
    dt on a successful valid snapshot. A stale / not-ready period is reported
    and skipped so the loop keeps running on the last live values."""
    try:
        snap = dev.read_period_snapshot()
    except RbAmpStaleError:
        print("  * period STALE (skipped)")
        return
    except RbAmpError as e:
        print(f"  * period skipped: {type(e).__name__} -- {e}")
        return
    print(
        f"  * period ok: avg_P0={snap.avg_p[0]:6.2f}W  "
        f"dt={snap.master_dt_ms:5d}ms  ->  E={dev.energy.wh(0):.4f}Wh"
    )
def run(bus, addr=0x50, ct_code=MY_CT,
        live_s=LIVE_S, energy_s=ENERGY_S, max_iters=None):
    """Core loop; broken out so a test harness can swap in a mock bus."""
    with RbAmp(bus, addr) as dev:
        print(
            f"rbAmp @ 0x{dev.address:02X}  fw=0x{dev.firmware_version:02X}  "
            f"topology={dev.topology_name}  channels={dev.channels}  "
            f"voltage_hw={'yes' if dev.has_voltage_hw else 'no'}"
        )
        ensure_ct_preset(dev, ct_code)
        print("Accuracy: +/-0.5% of reading with matched CT. Ctrl-C to stop.\n")
        last = {"V": 0.0, "I": 0.0, "P": 0.0, "PF": 0.0}
        counters = {"ok": 0, "bad": 0}
        next_live_t   = time.monotonic()
        next_energy_t = next_live_t + energy_s
        iters = 0
        while True:
            now = time.monotonic()
            if now >= next_live_t:
                next_live_t = now + live_s
                read_live(dev, last, counters)
            if now >= next_energy_t:
                next_energy_t = now + energy_s
                roll_energy(dev)
            iters += 1
            if max_iters is not None and iters >= max_iters:
                return counters
            sleep_for = min(next_live_t, next_energy_t) - time.monotonic()
            if sleep_for > 0:
                time.sleep(sleep_for)
def main():
    ap = argparse.ArgumentParser(
        description="Standalone Raspberry Pi energy monitor for a single rbAmp UI1 module."
    )
    ap.add_argument("--bus",  type=int, default=1,
                    help="I2C bus number (Raspberry Pi default: 1)")
    ap.add_argument("--addr", type=lambda s: int(s, 0), default=0x50,
                    help="rbAmp I2C address (default: 0x50)")
    ap.add_argument("--ct",   type=int, default=MY_CT,
                    help="CT model code: 1=SCT-013-005, 2=-010, 3=-030, "
                         "4=-050, 6=-020 (default: 3 = 30 A)")
    args = ap.parse_args()
    with SMBus(args.bus) as bus:
        run(bus, addr=args.addr, ct_code=args.ct)
if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nstopped.")

(Run-verified against the rbAmp Python library's test backend on CPython 3.12 — rbamp 1.3.0 + smbus2 0.6.1. set_ct_model_ch() persists to flash, so the script only re-writes the CT preset when it actually differs — no flash wear on every run. The run() core is broken out so it's easy to test or reuse.)

What the script is doing

The whole rbAmp Python library fits in a couple of with blocks and a handful of reads.

The context managers

python
with SMBus(1) as bus:
    with RbAmp(bus, 0x50) as dev:
        ...

SMBus(1) opens the Pi's I²C bus; RbAmp(bus, 0x50) binds the library to the module at address 0x50. Both are context managers, so the bus and the device are cleaned up automatically when the block exits (or on Ctrl-C) — the Pythonic version of "open the bus, probe the module."

The live reads

python
v  = dev.voltage
i  = dev.current[0]
p  = dev.power[0]
pf = dev.power_factor[0]

These properties return the module's real measurements: dev.power[0] is true active power in watts — the mean of instantaneous voltage × current, not current × an assumed voltage. (The method form dev.read_power(0) is exactly equivalent — use whichever reads cleaner.) A transient read can raise RbAmpError; the script catches it and keeps the previous good values rather than crashing.

Energy — the period snapshot

python
snap = dev.read_period_snapshot()   # atomic LATCH: close the window, read avg power
print(dev.energy.wh(0))             # host-integrated watt-hours, honest over wall-clock time

read_period_snapshot() sends the module an atomic LATCH — "close the measurement window now" — and the library rolls the host-side watt-hour total from average_power × elapsed_time. The Pi owns the integration interval, so there's no on-device counter to drift; dev.energy.wh(0) is an honest running total. A stale window raises RbAmpStaleError, which the script just skips.

What you'll see

In the terminal, a live readout that tracks the load — switch on a lamp or a kettle and watch the watts jump:

plaintext
rbAmp @ 0x50  fw=0x04  topology=SINGLE  channels=1  voltage_hw=yes
CT model already SCT_013 code=3; skipping flash write
Accuracy: +/-0.5% of reading with matched CT. Ctrl-C to stop.
U= 231.2V  I= 4.874A  P= 1123.5W  PF=+1.00  E=  0.0000Wh  [ok=1 bad=0]
U= 231.2V  I= 4.874A  P= 1123.5W  PF=+1.00  E=  0.0000Wh  [ok=2 bad=0]
  * period ok: avg_P0=1119.40W  dt=   50ms  ->  E=0.0622Wh
U= 231.2V  I= 4.874A  P= 1123.5W  PF=+1.00  E=  0.0622Wh  [ok=3 bad=0]

How accurate is it?

With the matched rbAmp CT, each channel is rated ±0.5% of reading across the calibrated range. The module measures true active power with per-channel phase compensation — so reactive and low-power-factor loads read honestly. The why-behind-the-numbers: how CT energy measurement really works.

Make it your own

  • Log it to a file or SQLite — the Pi has a real filesystem; append each reading to CSV or a database (that's the logger build).
  • Serve it — the Pi is networked: expose the readings as a tiny Flask endpoint, or publish over MQTT to your own broker — without any cloud.
  • A cron summary — run the script on a timer and email yourself a daily kWh total.
  • Add a display — a small I²C OLED on the same bus for a headless readout.
  • More circuits — put several modules on the bus and total them (that's the multi-module build).

None of these touch the measurement — they're what you do with the numbers the reads already give you.

Where to go next

This one module in the terminal is the foundation. From here the Python series builds up:

  • Meter several circuits — multiple rbAmp modules on one bus, read and totalled by the Pi.
  • Act on it — trip a GPIO relay when a circuit goes over a current limit.
  • Log it — periodic energy to CSV or SQLite.

(Prefer a microcontroller instead of a Pi? The same module runs on Arduino or in Home Assistant via ESPHome.)


Documentation & source code
📖 rbAmp Python library reference · 💻 rb-amp/rbamp-python on GitHub — issues, examples, ⭐
Log Your Energy Data to an SD Card with Arduino — Timestamped CSV, No Cloud