Skip to content

Use it

Python API·

Using the ring from Python: Ring, commands, codec, audio and export — the pieces ring-cli is made of.

  • people writing code against the ring
  • 1 min read
  • 76 words

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.