Use it
Python API·
Using the ring from Python: Ring, commands, codec, audio and export — the pieces ring-cli is made of.
In 10 seconds
ring-cli is a thin typer layer over six modules — transport · codec · commands · audio · export · bridge.
Anything the CLI does is a few lines of asyncio: open Ring(address), await C.get_battery(r).
Reference vectors for ports live in tests/test_codec.py.
import asyncio
from ring.transport import Ring, Evidence
from ring import commands as C
async def main():
async with Ring("<uuid-or-mac>", Evidence("evidence/mine.jsonl")) as r:
info = await C.get_device_info(r) # {'firmware': 152, 'mac': '…', …}
batt = await C.get_battery(r) # {'battery_percent': 100, 'charging': 1}
await C.handshake(r, auto_hr=True) # what LoraFit does after connect
ticks = await C.hr_live(r, seconds=30) # [{'ts': …, 'hr': 85}, …] (empty = off finger)
print(info, batt, ticks)
asyncio.run(main())
ring.transport — connect, send, listen
| | |
|---|---|
| Ring(address, evidence=None, verbose=False) | async context manager. __aenter__ first tries the system-held link (macOS bond, ~10 ms), then scans up to 45 s. Subscribes to 33F4. |
| await r.send(cmd, payload=b"", *, reply=None, timeout=5.0) | build a frame, pace it ≥ r.write_gap (0.3 s), write, and wait for the reply cmd (None = same cmd, -1 = don't wait). Returns the decoded reply. |
| r.listeners: list[Callable[[Frame, dict], None]] | every parsed notification (frame + decoded dict) — how the bridge sees live HR, battery, button. r.raw_listeners gets bytes. |
| r.start_heartbeat(period=2.0) / stop_heartbeat() | cmd 62 keep-alive task (needed for audio). |
| await r.dump_gatt(read=True) | the whole GATT table with values — ring-cli gatt. |
| await r.read_battery_bas() | standard 2A19 — lies, and can trigger the macOS bond; prefer cmd 6. |
| await system_connected_rings(address=None) | rings macOS already holds (retrieveConnectedPeripheralsWithServices([56FF])). |
| await find_ring(timeout=20, address=None) | scan by manufacturer id 0x594A / name; is_ring(), decode_mfr() are the filters. |
| Evidence(path) | append-only JSONL: {ts, dir, cmd, hex, decoded, …} per frame. Every ✅ in the docs is one of these lines. |
ring.commands — one coroutine per LoraFit request
Each returns the decoded reply: set_time, get_battery, get_device_info, get_features, get_steps,
get_step_history(day), get_sleep(day), get_health_record(day), hr_live(seconds, on_sample),
spo2_measure, temperature_measure, find_ring(mode), set_auto_hr(...), get_alarms, set_alarms,
audio_state, offline_file_count, heartbeat, set_app_info, raw(cmd, payload_hex, reply) (refuses 39/67
unless i_own_this_ring), and handshake(r, auto_hr, interval_min, heartbeat) — the whole post-connect
sequence. History helpers collect items until the terminator (42/43/45) or a 3 s idle.
ring.codec — frames, decoders, timestamps
| | |
|---|---|
| Cmd | the command ids as constants (Cmd.OPEN_HR = 7, Cmd.DEVICE_OPERATION = 39 …) |
| build(cmd, payload) → bytes | one request frame |
| parse(data) → Frame | header + payload; Frame.encode(), .last, .hex() |
| parse_headerless(data) | the 13-byte connect echo (0c fw mac 3a00 4800) |
| decode(cmd, payload) → dict | typed decoding for every known reply (battery, devinfo, HR tick, history items…) |
| device_ts(ts_utc=None) / read_device_ts(b, off) | the 6-byte local-seconds stamp both ways (Time) |
ring.audio — memo pull, live record, Ogg Opus muxer
query_state(r), file_count(r), parse_audio_payload(frame) → AudioPacket(seq, frames),
audio_state_bits(state), write_ogg_opus(path, frames, comments) (pure-python Ogg muxer), to_wav(opus_path)
(ffmpeg, optional), pull_offline(r, out, clear, max_s), record_online(r, seconds, out, mode).
ring.export — the tiny sample contract, posted
sample(metric, value, ts, raw) plus from_hr / from_spo2 / from_temperature / from_battery / from_button /
from_step_history / from_health_record / from_sleep build rows; post(samples, device) →
POST /api/health/ingest, post_media(path, …) → POST /api/health/media, both with ~/.tiny/device.json
{deviceId, token} in the body and a fallback JSONL queue for 5xx. Row shape: tiny → Health platform.
ring.bridge — the daemon
Bridge(address, evidence, days, flush_s, resync_s, post, auto_hr_min) with await b.run() — behind
ring-cli bridge (Bridge). Subclass or wrap it to build a gateway on another host.