Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

meter_tui

A live terminal dashboard for rtlamr JSON output.

Every utility meter your SDR hears (R900, SCM, SCM+, IDM, NetIDM) gets a row with its latest consumption reading, deltas, and a live sparkline. Pick a row, press Enter, and you get a full-size ASCII chart plus a scrollable table of that meter's readings. Every reading is also written to disk, so a long capture doubles as a permanent history.

Pure Python 3 standard library — no pip installs, no dependencies beyond curses.


Screenshots

Main table — all meters at a glance

 rtlamr live meters  —  q quit   ↑/↓ select   enter: chart
TYPE   ID                 LATEST     DELTA         TOTAL     AGE      N  GRAPH
R900   702282128       3,088,108       +18          +142    154s    133                          ▄▇
SCM    37603849          469,395        +1            +4    235s    134                         ▂▄▆
SCM+   76074285          475,854        +2            +6    205s     97                          ▃▅
R900   706074696          42,642       +40        +2,187    171s    134                    ▁▂▄▅▅▆▆█
R900   703681020         543,509        +3           +29    167s    134                           ▇
R900   706131386       1,039,463        +7          +185    199s    124                    ▁▃▃▃▄▄▄█
SCM    38091037           31,595        +0            +0    263s    125
R900   705818772         702,628       +15           +33    145s    109                          ▃▄
R900   702296402       3,833,030       +31          +265    210s    109                       ▁▂▅▇▇
SCM    37603885          860,185        +1            +1    181s    154
R900   702282436       2,063,294        +0            +0    200s    121
SCM    33462799          526,763        +1            +1    184s    118
R900   702282012       3,466,975        +0            +0    1.9h     13
SCM    14520144          450,760       +20           +25    322s     14                           ▂
R900   702222846       6,001,169       +65          +152    231s     25                        ▁▃▄▅
R900   702286900       3,341,656       +73          +277    353s      3                           ▆
SCM    33462781          784,059        +1            +1    1.5h      2
R900   702282026       4,692,794        +7          +139    505s     30                    ▁▂▂▅▆▇▇█
R900   706192078         509,632       +16           +20    202s     40                         ▁▁▂
SCM    38091041          563,132        +1            +1    168s     77
SCM    37601932          270,081        +3            +8    431s     18                          ▄▅
R900   702288500       3,588,777        +1           +36    252s      5                           █
R900   702207190       2,567,391        +0            +0    2.0h      1
replay finished — press q to quit
Column Meaning
TYPE Meter protocol (R900, SCM, SCM+, IDM, NetIDM)
ID Meter/endpoint ID, normalized across protocols (see Protocols)
LATEST Most recent consumption value
DELTA Change vs. the previous different value
TOTAL Consumption since the earliest reading this run has seen for that meter
AGE How long ago that meter was last heard from
N Number of readings received
GRAPH Sparkline of the meter's history (repeated values collapsed)

Detail view — press Enter on a row

 R900 706074696  —  134/134 shown [all points]   esc: back   ↑/↓ scroll   g/G top/bottom   c: toggle changes-only
  42,642                                                                                          ●●●●●●●●●●●●●●●●●●●
                                                                                                  │
                                                                                                  │
                                                                                                  │
                                                                                   ●●●●●●●●●●●●●●●
                                                  ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●
                                                  │
                                                  │
                                    ●●●●●●●●●●●●●●
                                    │
                                    │
                    ●●●●●●●●●●●●●●●●
                    │
                    │
  40,753 ●●●●●●●●●●●
latest: 42,642   last seen: 2026-08-10T20:20:02.399255221Z (205s ago)   points: 134   total since first seen: +2,187
TIME                   CONSUMPTION     DELTA
2026-08-10 20:20:02         42,642        +0
2026-08-10 20:18:41         42,642        +0
2026-08-10 20:18:01         42,642        +0
2026-08-10 20:16:38         42,642        +0
2026-08-10 20:15:17         42,642       +40
2026-08-10 20:14:37         42,602        +0
2026-08-10 20:13:16         42,602        +0
rows 1-7 of 134   ↑/↓ scroll · g/G top/bottom   (134 received total)

Detail view, "changes only" — press c

Meters broadcast the same value over and over between actual usage ticks. c filters both the chart and the table down to the readings where the number actually moved — here, 134 raw readings collapse to the 10 that changed:

 R900 706074696  —  10/134 shown [changes only]   esc: back   ↑/↓ scroll   g/G top/bottom   c: toggle changes-only
  42,642         ●●
                 │
                 │
                ●
              ●●
             ●
             │
            ●
            │
            │
           ●
           │
          ●
          │
  40,455 ●
latest: 42,642   last seen: 2026-08-10T20:20:02.399255221Z (206s ago)   points: 134   total since first seen: +2,187
TIME                   CONSUMPTION     DELTA
2026-08-10 20:15:17         42,642       +40
2026-08-10 20:00:08         42,602      +479
2026-08-10 19:45:19         42,123      +149
2026-08-10 19:34:33         41,974       +20
2026-08-10 19:15:40         41,954       +22
2026-08-10 19:00:16         41,932      +335
2026-08-10 18:45:27         41,597      +470
rows 1-7 of 10   ↑/↓ scroll · g/G top/bottom   (134 received total)

latest, points, and total since first seen always describe the real underlying data — the filter only affects what's plotted and listed.


Running it

Live, straight from rtlamr

rtl_tcp &
rtlamr -format=json | python3 meter_tui.py

rtlamr's startup banner (SampleRate, DataRate, Preambles, GainCount…) is plain text, not JSON, so it's skipped automatically — including when you merge stderr in:

rtlamr -format=json 2>&1 | python3 meter_tui.py

Replay a capture file

python3 meter_tui.py -f clean.jsonl                     # load it all instantly
python3 meter_tui.py -f clean.jsonl --replay-delay 0.02 # pace it like a live feed

Follow a file that's still growing

tail -f -n +1 live.jsonl | python3 meter_tui.py

Backfill from several files, then keep going live

Files are read in the order given, so history is in place before the live stream takes over:

python3 meter_tui.py -f day1.jsonl day2.jsonl day3.jsonl
rtlamr -format=json | python3 meter_tui.py -f old1.jsonl old2.jsonl

Log everything to per-meter, per-day files

rtlamr -format=json | python3 meter_tui.py --out-dir output/

Other options

python3 meter_tui.py -f clean.jsonl --history 1000   # keep 1000 points per meter in memory
python3 meter_tui.py -f clean.jsonl --no-log         # don't append to meter_tui_log.jsonl
python3 meter_tui.py -f clean.jsonl --log run2.jsonl # append somewhere else
python3 meter_tui.py --debug-keys                    # diagnose arrow keys over SSH

Controls

Key Action
q Quit
/ , k / j Select a meter row (table) · scroll readings (detail view)
Enter Open the big chart for the selected meter
c Detail view: toggle "changes only" (hide repeated values)
g / G Detail view: jump to newest / oldest reading
Esc / Backspace / b Back to the table
+ / - Keep more / less history per meter (±50, default 300)

Data written to disk

--log (on by default)

Every reading that parses as JSON and matches a supported meter type is appended verbatim to meter_tui_log.jsonl. Banner noise is filtered out, but nothing real is dropped — repeats included — so the log is a faithful capture of what rtlamr sent. It's line-buffered, so a hard kill can't lose buffered readings. --no-log turns it off.

--out-dir DIR (off by default)

Additionally fans each reading out to DIR/<ID>_<TYPE>_<YYYY-MM-DD>.json — one JSON array per meter per UTC day, kept sorted by time:

output/
├── 14520144_SCM_2026-08-10.json
├── 37603849_SCM_2026-08-10.json
├── 702207190_R900_2026-08-10.json
└── 76074285_SCM+_2026-08-10.json
[
  {
    "Time": "2026-08-10T17:51:34.312530572Z",
    "Consumption": 469391
  },
  {
    "Time": "2026-08-10T17:52:35.251882753Z",
    "Consumption": 469391
  }
]

Existing files are loaded and merged on startup, so these accumulate across runs — a permanent on-disk history independent of --history's in-memory cap.


script/dedupe.py

Overlapping backfill files (or replaying the same capture twice) put duplicate readings in the log. dedupe.py sorts a log by timestamp and drops any line whose Time field duplicates an earlier one:

python3 script/dedupe.py meter_tui_log.jsonl                 # print to stdout
python3 script/dedupe.py meter_tui_log.jsonl -o clean.jsonl  # write a new file
python3 script/dedupe.py meter_tui_log.jsonl --in-place      # overwrite (keeps .bak)
24282 lines read, 0 malformed skipped, 22523 duplicate timestamps removed, 1759 lines written to clean.jsonl

Protocols

Field names differ per protocol, so ID and consumption are normalized:

Type ID field Consumption field
SCM, R900 Message.ID Message.Consumption
SCM+ Message.EndpointID Message.Consumption
IDM, NetIDM Message.ERTSerialNumber Message.LastConsumptionCount

Records of any other type, or missing an ID or consumption value, are ignored.


Notes and troubleshooting

Keyboard works with a piped stdin. In rtlamr … | python3 meter_tui.py, fd 0 is rtlamr's output, not your keyboard — curses would "type" whatever bytes rtlamr sends. The script dups the piped fd for its reader thread and rebinds fd 0 to /dev/tty, so keypresses come from the real terminal. Nothing to configure.

Arrow keys over SSH / tmux. Arrow escape sequences are decoded by hand (both the ESC [ A and ESC O A forms) rather than trusting the remote host's terminfo entry for $TERM, which is a common failure mode over SSH and through multiplexers. If keys still misbehave, run python3 meter_tui.py --debug-keys in that exact session to see what your terminal is actually sending:

Key debug mode -- press Up/Down/Enter/Esc, q to quit

code=259    keyname=KEY_UP         KEY_UP
code=258    keyname=KEY_DOWN       KEY_DOWN
code=10     keyname=^J

Timestamps. rtlamr emits 0–9 fractional-second digits, which datetime.fromisoformat doesn't handle consistently across Python versions, so they're parsed with a dedicated regex. Everything is UTC.

Terminal size. The layout adapts: the sparkline column takes whatever width is left over, and the detail view splits its space between chart and table (table capped at 12 rows, chart gets the rest). Wider terminals show more history; ~100 columns is a comfortable minimum.

"No input" — the script exits with a message if stdin is a terminal and no -f is given. Pipe something in or pass a file.

About

Small TUI utility to helping find your meter in a sea of other meters...

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages