Reolink Camera Operator
Overview
Primary surface: reolink-cli (JSON stdout by default). Don't start the MCP server unless asked. When the user needs non-CLI access (browser, curl, other languages), point them at the gateway's POST /api.
Slash commands (Claude Code only) — prefer these for fixed/single-action intents; skip the intent-mapping overhead:
| Slash | Equivalent | When to use |
|---|---|---|
/reolink-cli:status |
reolink-cli status |
user asks about current state / dashboard / "how are things" |
/reolink-cli:features |
reolink-cli features |
user asks what the plugin can do / "what can it do" |
/reolink-cli:scan |
reolink-cli discover |
scan local network / "scan". Finds only devices that are awake and have their own LAN address — a sleeping battery camera does not answer (the responder lives in the main application, which is powered down), and a camera paired to a hub/NVR has no address at all: use device expand <parent> for those. |
/reolink-cli:devices |
reolink-cli device list |
list registered cameras |
/reolink-cli:cache-clean |
reolink-cli cache clean dry-run→apply |
clear old snapshots / "clear cache" |
/reolink-cli:update |
reolink-cli self-update |
upgrade to the latest release (checks GitHub, no-op if current) |
/reolink-cli:uninstall |
reolink-cli setup --uninstall --purge --no-interactive |
complete uninstall |
Claude Code runtime: Must use the slash command when the user's intent matches one of these seven exactly — faster (no LLM latency), deterministic, and discoverable via / autocompletion.
Other agents (Codex / Cursor / Copilot / Gemini): these surfaces don't support plugin-defined slash commands, so the "slash-first" rule doesn't apply — skip straight to running the equivalent reolink-cli … CLI command directly. Skill-driven intent mapping handles everything else.
All agents: Fall back to skill-driven CLI chaining for compound / fuzzy intents ("announce when a person is detected", "make it brighter") that aren't covered by the slash list / seven fixed actions above.
The gateway is mandatory for almost every control command. The CLI routes through 127.0.0.1:9000 by default (set in config.toml under the platform config dir — ~/.config/reolink-cli/config.toml on Linux, ~/Library/Application Support/reolink-cli/config.toml on macOS, %APPDATA%\reolink-cli\config.toml on Windows — as gateway-addr). If you see gateway connect failed: Connection refused, the gateway isn't running — start it once:
reolink-cli gateway start --addr 127.0.0.1:9000 &
Works without the gateway: device add|list|update|remove|resolve|show, config init, discover, features, doctor, cache status|clean — everything that reads local config or talks to the network directly. (Verified by running each with no gateway listening.) Everything else (ping, login, info, config get/set, image, osd, ptz, detect, light, audio, preview, snapshot, vod, events, users, system reboot, system upgrade) needs the gateway up.
Envelopes — parse .ok on CLI, .status on gateway:
| Surface | Success | Error |
|---|---|---|
| CLI | {ok:true, command, protocol, data} |
{ok:false, error:{code, message, retryable}} (stderr) |
| CLI batch | {command, summary, results:[{camera, host, ok, data, error}]} |
per-target ok |
Gateway /api |
{status:"success", data} |
{status:"error", code, message} (HTTP status == code) |
references/ has topic-scoped recipe files. Must load only the one matching your task — references/index.md is the router. references/troubleshooting.md has diagnosis decision trees for failures.
Ground Truth (Binding)
Forbidden stating or implying as factual: device reachability, login state, configuration values, capabilities, detection/AI types, event counts, VOD file existence, user accounts, or any observable camera state — except when derived from the JSON output of an executed reolink-cli command or gateway POST /api response within the current task.
Forbidden: demo-style lists, guessed layouts, synthetic values, fake success after errors/timeouts, prose/JSON mimicking CLI output without a real response, or "the device has X" when no get/info/inventory/capabilities has run.
Must when data is missing: state what's missing and the cause (no camera registered, gateway down, auth failed, device offline), then take one concrete recovery step (from Error Recovery table) or ask a single clarifying question. Forbidden padding with plausible-sounding defaults.
Intent Interpretation
Resolve ambiguity before picking a command. If still unclear, ask with options.
| User says | Command | Why / caveat |
|---|---|---|
| add a user | users add |
Device account, NOT a CLI camera entry |
| add a camera | device add |
CLI camera entry (~/.config/reolink-cli/aliases.toml), NOT a device account |
| change password | users passwd (default) OR device update (CLI creds) |
Ask which |
| blinking light | light statusled |
Body LED |
| night vision | light ir |
Invisible IR LED |
| spotlight | light spotlight (manual) OR light whiteled (alarm-triggered) |
Same bulb, different wiring |
| too dark | image tune --bright first, check light ir get if night |
Image tuning, not spotlight |
| upside down | image flip set --flip |
Vertical |
| mirrored | image flip set --mirror |
Horizontal |
| mask | privacy mask |
Read current regions first |
| rename | osd set --name (on-screen) OR config set device-name (app) |
Ask which |
| restart / reboot | system reboot |
30–60s offline |
| upgrade / flash firmware / OTA | system upgrade <file.pak> |
⚠️ bricking risk. Match model+hw_ver via info; GATEWAY reads the LOCAL file; auto windowed/stop-and-wait. Detail in references/admin.md |
| factory reset | — standalone; system upgrade --factory-reset resets during a flash |
Standalone reset not in CLI; ask user to use Reolink app |
| kick off | users remove (permanent) OR users passwd + system reboot (evict session) |
Active sessions survive users remove until reboot |
| snapshot | snapshot [--file PATH] |
JPEG. Parent directories are auto-created by the binary (since v0.5.0) — do NOT pre-mkdir -p the destination. Same for vod download and preview capture --file. |
| slow / how long / time taken / performance / latency / benchmark | benchmark [--iterations N] [--phases ...] [--reuse-session] |
Per-phase p50/p95/p99: connect, login, info, snapshot. --reuse-session for warm-path. Detail in references/index.md. |
| cpu / memory / device load / is the camera overloaded / stuttering | config get performance |
Live reading from the device: cpuUsedPercent, codeRate, netDataRate. Read-only and instantaneous — two calls a second apart legitimately differ. Not every model implements it; those answer a device-level rejection. Distinct from benchmark, which times the client round trip, not the camera. |
| rtsp / rtmp / flv address / stream URL / HA / Frigate / go2rtc / VLC | stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth] |
Default --kind rtsp --stream main. --with-auth only when user explicitly wants one-shot pasteable URL. NVR: device expand then --tag <nvr>. Detail in references/media.md. |
| nvr with 8 channels / RLN sub-cameras / channel N / cameras paired to a Home Hub | device expand <parent-name> [--yes | --names A,B,C] |
Any v20 parent — RLN NVRs and a Home Hub alike. Registers one entry per populated channel, tagged with the parent name; after: --tag <parent> fans out. This is the only way to reach a battery camera paired to a hub — it has no address of its own, so discover cannot see it. Refused for protocol=v30 entries. |
| command rejected 400 / "model doesn't support it" / battery camera on a hub / first command after a pause | Nothing — the gateway already retries. attempts in the answer says which try got through (3 is normal after a pause, 1 during a burst) |
A battery child behind a hub sleeps and then rejects commands with a bare 400, indistinguishable from unsupported. When it sleeps is not predictable from idle time alone (one day's readings said info → channel.loginState = standby explains it — but that field lags, so it explains a failure, it cannot pre-flight one. |
| dual-lens / two lenses / bullet+PTZ in one camera / wide and telephoto / second lens | info first (channel.views, channel.dualLens), then --view N on config commands |
--view is a third axis: --channel picks the camera, --view picks the lens, --stream picks that lens's encoding profile. Verified per-view: encode, privacy mask, detect motion, PTZ. Not per-view: snapshot (device answers 400 for view≥1), preview/stream url (protocol has no selector — these now reject --view N rather than silently handing back view 0), image/ISP (shared). Default 0 = the only view an ordinary camera has. |
| preview / take a look / watch N minutes / watch live | preview play (opens ffplay window) |
DEFAULT to play, not capture. User wants a live window, not a file. |
| record N seconds / save a clip / save it | preview capture --packets ≈N×25 |
Only when user explicitly says save / record / export. Packet count, not seconds; ~25 fps main, ~10 fps sub |
| 60-second preview (ambiguous!) | If user says "preview for N seconds/minutes" → preview play --packets N×fps. If user says "record N seconds" → preview capture. Ask if truly unclear. |
Default to play when the verb is "preview/watch", capture only when the verb is "record/save/export" |
| volume | audio volume set (master) |
Ask if they mean mute: audio mute |
| mute alarm | audio mute |
Alarm audio only, not preview |
| siren / alarm sound / sound the alarm / scare them off / make it beep | audio siren play [--duration N | --times N] [--wait] |
Loud + side-effectful — confirm first. cmd 263 manual ring-down (the app's siren button); device self-stops after --duration s, or audio siren stop. --wait holds for the duration then stops explicitly and reports stoppedAt + stopAcknowledged — use it when you must be able to say the siren stopped, not just that it started. A 400 from a battery camera behind a hub is not evidence about the hardware — a sleeping child answers 400 exactly like an unimplemented command. (Separately: on one Home Hub 2 the siren was measured not to sound at all even when the command succeeds — see verified: false below. Never claim it sounded.) The gateway retries (measured: a camera idle >2 min needs 3 attempts, ~1.6 s), and attempts in the answer tells you which try got through. If it still fails, info → channel.loginState = standby explains it — but that field lags, so it is for explaining a failure, not for checking before one. Never tell a user their camera lacks a feature on the strength of a 400; capabilities is what answers that. Same rule for light spotlight. "siren when motion is detected" is different — that is audio siren task linkage (often 405 on newer models). |
| arm / disarm / away / at home / i'm leaving / scene mode / stop recording while i'm in | scene set <id> (scene list for the ids, scene set --schedule to hand back to the timetable) |
Hub/NVR only. One switch re-arms every channel — it does not touch per-camera detection config. scene show first: currentSceneId 0 means the timetable is driving. Detail in references/scene-mode.md. |
| resolution / bitrate / fps / frame rate / h265 / h264 / stream quality / make the stream smaller | encode set --stream main|sub|third ... |
Run encode capability first — a value the device does not advertise comes back as a bare 400. stream url is unrelated (that only builds a playback URL). |
| person detection | detect ai --type person |
Check device inventory --capabilities first |
| vehicle | detect ai --type vehicle |
Not all models |
| turn on recording | record schedule set --enable |
|
| record config / recording params / pre-record seconds / clip size | record config get |
Read-only; cycle / pre/post record / package time |
| SD card / storage card / capacity / free space | storage status |
Read-only; totalGB / remainGB / formatted / mounted |
| manual record / record now / start recording | — | Not supported on Reolink IPCs; fall back to record schedule set --enable/--disable |
| download yesterday / download recording | vod search --since 24h → vod download NAME |
|
| download 09:45–09:55 / just that clip / a time range | vod download --from 2026-09-02T09:45:00 --to 2026-09-02T09:55:00 |
Camera-side cut — do NOT download the whole hour and trim |
| any alarms / any alarms today | events query --since 24h |
Requires gateway |
| event history further back than the live buffer / what happened last week / hub event log | --channel N events history --since 7d [--types people,motion] (hub/NVR only) |
Reads the device's recorded event log (cmd 516/517), not the ~500-entry live ring. Standalone IPCs 400. Empty list = no matching events; a hub can go briefly quiet after heavy use, retry after a pause. |
| voice alert / voice announcement / announce when someone arrives / play voice when a person is detected | audio talk (see references/voice-alert.md) |
PCM16 LE mono only; needs capabilities.audioTalk=1 |
| detect X do Y / automation / event trigger / detect-then-do | Must events monitor init/check/run with TOML rule. Forbidden ad-hoc bash while + events query loops — see references/event-monitor.md. |
|
| is the monitor running / has a rule fired / monitor status / what fired recently | events monitor status (pid + cursor + lastFires + per-rule expired/secondsRemaining) or events monitor history --last N [--rule NAME] [--since 30m] |
Inspect the running daemon without restarting. Filters apply before --last, so --rule watch --last 20 is that rule's last 20 fires, not whatever survives the global tail. |
| watch this camera for the next 30 minutes / temporary rule / bounded monitoring task | add a rule with expires_at = "<RFC 3339 or Unix second>", events monitor reload, then read back with events monitor history --rule NAME |
Absolute time only — a relative "30m" would restart its countdown on every reload. Expiry stops the rule firing; it does not rewrite the rules file, and captured history is kept. |
| apply edited rules / reload rules | events monitor reload (Unix SIGHUP) |
Re-reads TOML without dropping cursor. Invalid TOML keeps old rules + logs warn. On Windows, stop + run again — SIGHUP is Unix-only. |
what can it do / what features / what's supported / what can this CLI do / what's new |
reolink-cli features (add --output text for human view) |
Lists installed commands + highlights since v0.2.8 + XDG paths. Must run this first to gate which subcommands exist on the user's binary |
| how much disk used / cache size / cache usage | reolink-cli cache status |
Inspect ~/.cache/reolink-cli/ by category (snapshots/audio/captures/downloads/logs) |
| current status / how are things / system health / overview / dashboard | reolink-cli status |
Fleet + gateway + events-monitor + cache. --camera X to include recent events. |
| is the gateway running / gateway status | reolink-cli gateway status |
500ms TCP probe; on [DOWN] prints exact gateway start to run. |
| check / diagnose / doctor / what's wrong / sanity check | reolink-cli doctor |
9 offline checks (binary, dirs, config, registry, perms, gateway TCP). Use FIRST when user says "broken". |
| tab completion / shell completion | reolink-cli completions {bash|zsh|fish|powershell|elvish} > <path> |
Re-run after self-update. |
| first-time setup / configure camera / set up reolink / how to use after install / how to get started | reolink-cli config init then device add |
config init writes default gateway addr; device add <alias> --host <ip> --user admin registers the first camera (password via prompt or --password-stdin). Then --alias <name> login to verify. |
| uninstall / delete / remove reolink | reolink-cli setup --uninstall --purge --no-interactive |
Confirm with the user before running this — --purge deletes their camera registry and cannot be undone. Without a terminal on stdin — which is every command you launch — the uninstall refuses unless --no-interactive is present; that flag is the declaration that the deletion was intended, so do not add it reflexively to make an error go away. Removes binaries + config/cache/state + cross-agent skill dirs + Claude Code plugin registry. Drop --purge to keep config/cache/state (alias list preserved) — there is no --keep-config flag. The agent skill dirs are global (~/.claude/skills/…, ~/.agents/skills/…): they are removed regardless of REOLINK_PREFIX, so uninstalling one copy of a side-by-side install unlinks the skill for both. If the customer still has the extracted tarball, ./uninstall.sh in there is the symmetric alternative (it forwards straight to this same command; the uninstall flow only honours --purge, --no-interactive and --prefix). (Note: npx skills remove reolink-cli is rarely needed — reolink-cli setup --uninstall already wipes the agent skill dirs.) |
| skill stale / plugin cache stale / refresh skill | reolink-cli plugin refresh |
Auto-detects agent (Claude Code / Codex / Cursor / Copilot / Gemini). Run when features reports cache_state != in_sync. |
| clean up / clear cache / delete old snapshots / clean cache | cache clean [--older-than 7d] [--category X] [--apply] |
Dry-run by default — always preview first, run --apply only after the user sees the list |
Workflow
Default: just run the command. The gateway daemon caches auth across CLI invocations, so the agent doesn't pay ping+login per command. Most user intents map to one CLI call (info, get, apply, snapshot, stream url, …).
Forbidden pre-fetching <subcmd> --help to "check what flags exist." The Intent Interpretation and Command Reference sections below, plus references/<topic>.md, already list every subcommand and its key flags; reolink-cli features enumerates the installed surface at runtime. Must dispatch the operative command directly. Only read --help if the command actually errored with an unknown-flag clap message. Each unnecessary --help round adds one full agent turn (~2–3 s of perceived user latency) for zero information gain — the user feels it, the CLI doesn't.
Must batch a single user intent into a single shell invocation when it needs multiple CLI commands. For sequences like "PTZ full sweep" (right→down→left→up→stop), "snapshot before/after a move", "info + capabilities + storage status", or "health check + benchmark" / "doctor + benchmark" pairs, chain them with && (or ; if you want continue-on-error) inside one Bash call — do not spawn N separate tool calls. Each extra tool call adds one full agent turn (~2–3 s). N=5 commands as one Bash = ~3 s perceived; N=5 commands as five tool calls = ~12 s perceived. The rule applies even when the commands are semantically distinct (e.g. doctor is local, benchmark hits the device) — the user asked for "both", so dispatch both in one shell line: reolink-cli doctor && reolink-cli --camera X benchmark --iterations 3. Independent prep checks for unrelated devices can still be parallel tool calls; the rule is about one intent → one invocation.
MCP server is the fastest path for repeated calls. reolink-cli mcp-server speaks JSON-RPC 2.0 over stdio (MCP protocol 2025-11-25, ~39 tools): identity (camera_{ping,login,info,capabilities,discover}), PTZ (camera_ptz_{move,stop,presets,preset_goto,preset_set,preset_delete}), light (camera_light_{ir,statusled}_{get,set}, camera_light_spotlight_set), audio (camera_audio_{volume_get,volume_set,mute}), detection (camera_detect_{motion,ai}_{get,set}), recording/storage (camera_record_config_get, camera_storage_status, camera_vod_search), events (camera_events_query), image/OSD (camera_image_{flip,tune}_get, camera_image_flip_set, camera_osd_{get,set}), users (camera_users_list), system (camera_system_reboot), plus camera_config_{get,set}, camera_preview_capture, camera_snapshot (JPEG to a file), and camera_raw. Benchmarked locally: 5 sequential camera_info calls take 52 ms via MCP vs 108 ms via 5 Bash spawns — ~52% faster, saving ~11 ms per call by avoiding process startup. When to suggest MCP wire-up: the user is going to do >3 ops in one conversation, or runs the agent in a tight loop. How to wire it in Claude Code (claude.json or settings):
"mcpServers": {
"reolink-cli": {
"command": "reolink-cli",
"args": ["mcp-server"],
"env": { "REOLINK_GATEWAY_ADDR": "127.0.0.1:9000" }
}
}
Pass alias, host, or uid per tool call to target a specific camera; the server validates input schemas and returns {structuredContent, isError} payloads. Caveat: the gateway must be running separately (MCP routes through it the same way the CLI does).
apply recipes are one-shot. For "set X to Y" intents, the *** apply subcommand does get → compare → (skip if idempotent) → set → verify internally and returns {before, after, changed, verified}. Forbidden running get → set → get manually, and forbidden running a separate get after an apply — the verify is already inside.
| Recipe | Replaces |
|---|---|
light ir apply --state auto|on|off |
manual ir get/set/get |
image flip apply [--flip|--no-flip] [--mirror|--no-mirror] |
manual flip get/set/get |
osd apply [--name] [--datetime] [--name-overlay] |
manual osd get/set/get |
detect motion apply [--enable] [--sensitivity N] |
manual motion get/set/get |
detect ai apply --type T [--sensitivity N] |
manual ai get/set/get |
ping and login are NOT pre-steps. They are diagnostics. The first command you actually need (info, get, apply, snapshot, …) does its own connect + auth via the gateway daemon. If that command's JSON has error.code == "auth_required", re-run login once and retry. If it has error.code == "connection_refused" / "timeout" / "no_route", that IS the same signal ping would give — no need to run ping again. Forbidden running ping followed by login followed by the real command on every turn; that's 3 round-trips for the work of 1. Only call ping standalone when the user explicitly asks "is camera X reachable?" or "why can't I connect?".
Verify only when needed. Re-get after a write only on capability-gated config SETs that can silently keep the old state (light ir/whiteled/spotlight set, image flip/tune set, audio config, detect motion/ai set, osd set, encode set, generic config set). Forbidden verifying after apply (already verified), ptz move/stop/preset, system reboot, system upgrade (confirm via info after the device reboots, not a re-get), snapshot, preview *, stream url, audio mute/unmute/talk, audio siren play/stop (a one-shot action, nothing to re-read), vod download, users passwd, privacy mask set. Full table in references/troubleshooting.md#verify-rules.
Other safety rules (kept verbatim — these break things if violated):
- Target selectors are global options (pre-subcommand). Forbidden positional. Priority:
--camera>--host>--uid> env. Batch:--tag,--cameras A,B,--all-devices. Must pass--channel Nfor NVR. - Credential safety: Forbidden
--password PLAINon argv. Must use--camera <name>(fromaliases.toml0600),REOLINK_PASSWORDenv, or--password-stdin. If no camera registered and op isn't trivially read-only, ask user to rundevice addfirst. - Stored passwords are encrypted (
RLENC1:…, AES-256-GCM) with the key incredentials.keybeside the file that holds the password — besidealiases.tomlin every layout, including a registry redirected with--cameras-file/REOLINK_CAMERAS_FILE, and beside a redirectedconfig.tomlif a password lives there. Separate registry files therefore have separate keys. A plaintext config is converted automatically on first use. Do not try to read a password out ofaliases.toml— it is ciphertext, and there is no command that reveals it. Backing up or moving a config means copying the file and thecredentials.keynext to it as a pair, from the same directory; with only one of them the passwords are unrecoverable and must be re-entered viadevice update <camera> --password-stdin. If a command reports a password that "cannot be decrypted", the key file is missing or mismatched — that is not a wrong-password problem, so do not retry with guesses. (Installs from before 0.12.4 kept a single key in the default config directory; it is still read as a fallback until the next write moves it beside its file.) - Pre-write read: Must
getcurrent value before any write that's NOT covered by anapplyrecipe; confirm side effects from the table below; verify NVR channel. - Protocol: leave it unset — it defaults to
v20, which the whole supported fleet speaks. A v30 camera declaresprotocol = "v30"in its entry (or--protocol v30for one call).--protocol autoasks the gateway to detect it from the device's response magic and costs an extra TCP round trip; use it only to re-detect a camera whose entry names the wrong protocol. - Battery devices on LAN (transparent wake): when
--uidis given without--host, the gateway automatically runs the BC3.0 §F.2 wake handshake on UDP 2026 before login (in customer / no-P2P builds). Forbidden callingreolink-cli wakeas a precondition — it is a hidden diagnostic command. Must just callinfo/preview/ptz/ etc. with--uidand accept that the first call may take 2–8 s while the device boots; subsequent calls within the gateway session TTL reuse the live session and have no wake overhead. - Events monitor exclusivity: when intent pairs trigger + action ("announce when a person is detected", "car detected → log"), Must use
events monitor init/check/runwith a TOML rule. Forbidden writingwhile true; do events query…; doneshell loops — the rule engine already has debounce, parallel dispatch, SIGTERM draining, cursor bootstrapping, action-timeout, retry. Seereferences/event-monitor.md.
Global Options
--host/--uid/--camera/--cameras A,B/--tag/--all-devices, --user/--password/--channel/--view, --protocol auto, --output json|text, --gateway-addr HOST:PORT, --config-file/--cameras-file. Env equivalents: REOLINK_HOST/UID/ALIAS/USER/PASSWORD/CHANNEL/VIEW/PROTOCOL/GATEWAY_ADDR/CONFIG_FILE/ALIASES_FILE. Run reolink-cli --help or <subcmd> --help for exact flag shapes.
Batch Operations
- Must keep the same selector throughout a workflow — the batch framework handles fan-out.
- Forbidden enumerating with
--tagthen switching to--camerainside the same workflow. - Batch output has per-target
ok; a single failure doesn't fail the batch. Must checksummary.failed > 0and iterateresults[].
Degenerate case: when a batch selector (--tag X, --cameras A, --all-devices) resolves to exactly one device, the CLI emits the single-target envelope ({ok, command, protocol, data}), not the batch report. Must test "summary" in response to detect which shape you got. Forbidden assuming results[] exists.
Error Recovery
| Error | Cause | Next step |
|---|---|---|
reolink-cli: command not found |
Binary not installed (installing the skill does not install the binary) | Run the install snippet in references/setup.md → Install the reolink-cli binary, then retry |
gateway connect failed: Connection refused |
Gateway not running | reolink-cli gateway start --addr 127.0.0.1:9000 & then retry |
ping returns reachable:false, reason:"timeout" |
IP not routable (cross-subnet / VPN not up / host off / firewall) | Tell user: verify VPN / check subnet; do not try login — it would hit the same timeout |
ping returns reachable:false, reason:"refused" |
Host reachable but port 9000 closed | Wrong port / wrong IP / gateway service not running on device |
ping returns reachable:false, reason:"no_route" |
No routing entry at all | Check netstat -rn / VPN |
ping returns skipped:true, reason:"uid only..." |
UID-only target | Expected — proceed directly to login |
device unreachable (retryable) |
Net drop / device off / wrong port | Re-ping; check sibling |
auth required / invalid credentials |
Wrong creds | device update; admin-reset via Reolink app if locked out |
invalid params |
Caller bug | Fix request, don't retry |
unsupported |
Firmware lacks feature | device inventory --capabilities to confirm; skip |
Connection reset after system reboot |
Expected | CLI swallows this one; re-ping after 30–60s |
| Remote (UID) login timeout | First-connect warm-up | Retry with --timeout-secs 15+ |
wake timed out after 3 attempts: no response from device with uid X |
Battery device not reachable on the local broadcast domain | Verify device is on the same /24 as the gateway host, UDP 2026 not firewalled, device not depleted; do not retry tightly — each attempt already takes ~3 s and the device may genuinely be off-network |
device acknowledged wake (booting) but did not report ready within 30s |
MCU woke but Linux main failed to come up | Physical-side issue (battery low, firmware stuck); user must check the device |
no active session (gateway) |
Token expired | Re-login via set auth.login |
| HTTP 401 (gateway) | Missing Authorization: Bearer |
Add header |
| HTTP 403 (gateway) | Token invalid/expired | Re-login |
HTTP 410 on /api/login or /api/request |
Old v0.1.1 endpoint | Use POST /api + Bearer |
users remove admin fails |
Device refuses removing last admin | Don't |
users passwd permission error |
Non-admin changing someone else's | Log in as admin |
Command Reference
Signatures only — run <cmd> --help for flag details; see references/<topic>.md for per-feature examples.
Discovery / Registry: discover, device list|resolve|show|add|update|remove|import|inventory|analyze|expand, config init
NVR / Hub multi-channel: device expand <parent-name> [--yes | --names A,B,C] [--drop-parent]. Any v20 parent (RLN-series and Home Hub); one entry per populated channel, auto-tagged with the parent name. A hub's paired battery cameras are only reachable this way — they have no LAN address, so discover never lists them. protocol=v30 entries are refused.
Identity: ping, login, info, capabilities
Config paths (config get/set PATH with merge semantics):
led, device-name, language, time-zone, time-format, network, osd, osd-format, system-general (get-only).
(The paths image / audio / alarm / alarm-policy / ai were removed — they were stubs. Use dedicated commands below.)
Preview / Snapshot: preview capture|play|start|stop, snapshot [-o FILE] [--stream]. --stream sub (the default) is the fast path — a small ~900x512 JPEG, ~2s on a woken camera; --stream main is a full-resolution frame at ~5s on a 4K model. A battery camera behind a hub sleeps (when is not predictable from idle time — see the 400 row above), and the first capture after it does waits for its whole video pipeline to boot: measured 5s (sub) to 23s (4K, three channels waking at once) before the device sends a single byte. That wait is device-side and unavoidable — it is not a hang, and the timeout budget covers it. A capture during a burst is sub-second; treat the first one after any gap as a warm-up rather than trying to predict which one pays the wake.
Benchmark: benchmark [--iterations N] [--phases connect,login,info,snapshot] [--reuse-session] [--warmup M] — per-phase p50/p95/p99/mean/stddev. Read-only.
Doctor: doctor — 9 sanity checks (binary, dirs, config.toml, registry, perms 0600, legacy cleanup, gateway TCP). Non-zero exit on fail. Use first for "broken" reports.
Completions: completions {bash|zsh|fish|powershell|elvish} — emits to stdout; redirect into shell's completion dir. Re-run after self-update.
Gateway status: gateway status — 500ms TCP probe of the resolved gateway-addr. Text shows [LISTENING] / [DOWN] / [UNCONFIGURED] + the gateway start command on down.
Gateway log files (since 0.12.5): the gateway always writes a compact single-line log to <cache-dir>/logs/gateway-<pid>.log (~/.cache/reolink-cli/logs/ on Linux, ~/Library/Caches/reolink-cli/logs/ on macOS, %LOCALAPPDATA%\reolink-cli\logs\ on Windows) at info level; files older than 14 days are pruned at startup, and cache clean --category logs manages them like any other cache category. **To collect evidence after a failed run: zip the logs/ directory — no pre-configuration needed.** For deep traces re-run with RUST_LOG=debug (adds per-round LAN probe results, every TCP connect attempt with address+outcome, uid-cache hits with age). REOLINK_LOG_FILE=off disables the file; REOLINK_LOG_FILE=<path> redirects it (user-managed, no pruning). Credentials never appear in logs; entries do contain hosts/UIDs/SSIDs, so redact before posting publicly.
Stream URLs: stream url [--kind rtsp,rtmp,flv] [--stream main,sub,ext] [--with-auth] — defaults to rtsp main. --with-auth embeds creds (use only when user wants pasteable URL); without it ships sibling user/password JSON fields. NVR: device expand then --tag <nvr> stream url. RTSP path is /Preview_{NN}_{main|sub|ext}. Detail in references/media.md.
Light: light {ir|statusled} {get|set --state auto|on|off}, light ir apply --state auto|on|off (recipe; prefer for "set X"), light spotlight set --enable|--disable [--duration], light whiteled {get|set [--enable|--disable] [--brightness 0-100] [--alarm-mode] [--detect-type T,...]}. Caveat: E-series can ok:true on light ir set while silently keeping old state — apply recipe surfaces this in verified:false; check device inventory --capabilities if write ignored.
Image: image flip {get|set|apply [--flip|--no-flip] [--mirror|--no-mirror]} (apply is the idempotent recipe), image tune {get|set [--bright N] [--contrast N] [--saturation N] [--hue N] [--sharpen N]} — all 0–255, 128 = neutral. If a user says "60%" that's ~160.
OSD: osd {get|set|apply [--name NAME] [--datetime|--no-datetime] [--name-overlay|--no-name-overlay]} (apply is the idempotent recipe)
System: system reboot; system upgrade <file.pak> [--factory-reset] [-y] — flash firmware (gateway reads the LOCAL file; ⚠️ bricking risk, match model+hw_ver; auto windowed/stop-and-wait by capability; detail in references/admin.md)
Users (device accounts): users list, users add NAME --level admin|user, users remove NAME, users passwd NAME. Password via TTY prompt or --password-stdin — never --password PLAIN. Name + password each 1–31 chars. Non-admin users can only change their own password.
Audio: audio config {get|set}, audio volume {get|set LEVEL} (0–100), audio mute, audio unmute, audio replies, audio talk {--file PATH | --stdin} [--sample-rate N]. audio volume is master; audio config.volume is per-profile — ask which. audio talk pushes PCM16 LE mono to camera speaker via talkback (cmd 201 open + 202 stream). Gate on capabilities.audioTalk=1.
Siren: audio siren play [--duration 1-600 | --times 1-100] [--wait] (default 2 repetitions ≈ 6s; device self-stops after the last one), audio siren stop (silence now). The device sounds in whole repetitions of ~3.2s, so --duration rounds up to --times and the answer reports both, plus speakerVolume — if the user says they heard nothing, read that first (a measured case sat at 7/100 and was inaudible; the command succeeded regardless). Up to 0.14.3 the siren never actually sounded — the CLI sent playMode 1 (by duration), which this firmware accepts with a 200 and ignores; playMode 0 (by times) is the one that makes noise, confirmed against the camera's own microphone. --wait blocks for the duration, then sends an explicit stop and adds waited/stoppedAt/stopAcknowledged to the answer; the firmware timer is still sent, so a killed CLI does not leave the siren running. Without --wait the answer is accepted: true, verified: false and nothing more — v20 has no GET for siren state, so never report that the siren sounded on the strength of it. This is the app's manual-siren button = v20 cmd 263 NET_MANUAL_RING_DOWN (<audioPlayInfo> continuous/switch mode), verified on real RLC-823/810. Gate on capabilities / JSON GetAbility.supportAudioAlarm. Also audio siren task {get|set --on|--off} (which alarms trigger it) + audio siren enable {get|set [--on|--off] [--channel-on|--channel-off]} — two switches: device-wide mainSwitch and a per-channel channelMask bit. A hub can report master-on with every channel bit zero; --on alone cannot fix that, which is why --channel-on exists — v20 cmds 306/307, 316/317, but this whole v3.x generation returns 405 for them; only manual play/stop is universal. A sounding device pushes cmd 547 → events stream shows siren.on/siren.off (no GET for siren state). NB: do NOT confuse with cmd 349 (play a stored audio file) — it 400s on cameras with an empty audio store, i.e. almost all of them.
Scene mode (hub arming profiles): scene show (active scene + which task bits this hub honours + the ids), scene list (every scene with its per-channel tasks), scene status, scene set <id> / scene set --schedule, scene edit <id> [--tasks record,ftp,email,push,audio,linkage,speaker,track] [--channel N] [--iot-tasks linkage,device] [--name ...] [--delay N], scene schedule get|set --scene <id> [--days 0,1] [--from H] [--to H], scene alarm get|set --task REC --types people,vehicle [--channel N], scene options [--key-enable|--key-disable] [--private-mode|--no-private-mode].
A scene is a named set of per-channel task bits, so switching scenes re-arms the whole hub at once — that is the app's Home / Away / Disarm control. audio in the task list is the siren, not the microphone. Scene id 0 is not a scene: it hands control to the weekly schedule, and scene show reports that as followSchedule: true. --tasks replaces the scene's set rather than adding to it, and without --channel it applies to every channel in the scene. The schedule is 7 days × 2 half-hour slots × 24 hours; --days takes the device's own 0-6 numbering.
Encoder: encode get (all three streams), encode capability (what the device accepts — read this before setting), encode set --stream main|sub|third [--resolution WxH] [--fps N] [--bitrate KBPS] [--codec h264|h265] [--rate-control vbr|cbr] [--profile default|baseline|main|high] [--gop N] [--smart-h265|--no-smart-h265] [--smart-h265-level 0-2]. The wire SET (cmd 57) rewrites all three streams at once, so the CLI always re-reads first and patches only the flags you pass. Changing resolution restarts the stream — any live
…(truncated)