Skill: pfrederiksen/rivian-ls (version 1.1.0)

--- name: rivian-ls description: Access Rivian vehicle telemetry (battery, range, charge state, locks, doors, tires, cabin temp, location) using the rivian-ls CLI tool. Use when the user asks about their Rivian's battery level, range, charging status, whether it's locked, door/frunk/liftgate state, tire pressures, odometer, or wants a vehicle status summary. Also use when building dashboards or automations that consume Rivian data. Requires rivian-ls installed and authenticated. --- # rivian-ls Fetch and work with Rivian vehicle telemetry via the [rivian-ls](https://github.com/pfrederiksen/rivian-ls) CLI. > ⚠️ Uses an unofficial Rivian API. May break without notice. Not affiliated with Rivian. ## Installation ```bash # From source (requires Go 1.21+) git clone https://github.com/pfrederiksen/rivian-ls.git cd rivian-ls && make build cp rivian-ls /usr/local/bin/ # Or via Homebrew brew install pfrederiksen/tap/rivian-ls ``` ## Authentication (Two-Phase MFA) Rivian requires MFA. Use the two-phase flow for non-interactive / scripted login: ```bash # Phase 1: Send credentials, triggers SMS code rivian-ls login --email [REDACTED_SECRET] --password secret # Phase 2: Complete with OTP after receiving SMS (within ~60 seconds) rivian-ls login --otp 123456 ``` Credentials cache to `~/.config/rivian-ls/credentials.json` and auto-refresh. After initial auth, subsequent runs use the cache automatically. **Non-interactive (single command, if OTP is known in advance):** ```bash rivian-ls --email [REDACTED_SECRET] --password secret --otp 123456 login ``` **Via environment variables:** ```bash export RIVIAN_EMAIL="[REDACTED_SECRET]" export RIVIAN_PASSWORD="secret" rivian-ls login # then provide OTP when prompted ``` ## Key Commands ```bash # Snapshot from cache (fast, no API call) rivian-ls status --offline --format json # Live fetch from Rivian API rivian-ls status --format json # Stream live updates (WebSocket, auto-falls back to polling) rivian-ls watch --format json # Export historical snapshots rivian-ls export --format json --since 24h # Multi-vehicle: select by index rivian-ls status --vehicle 1 --format json ``` ## Bundled Script Use `scripts/rivian_status.py` for clean text or JSON output: ```bash # Human-readable summary python3 scripts/rivian_status.py # JSON (pipe to jq, dashboard APIs, etc.) python3 scripts/rivian_status.py --format json # Force live fetch python3 scripts/rivian_status.py --live ``` ## Common Patterns **Status summary:** Run `python3 scripts/rivian_status.py` — covers all key fields. **Check if locked:** Parse `IsLocked` from JSON output. Alert if `false` and parked overnight. **Battery alert:** Check `BatteryLevel` and `RangeStatus`. Alert if below threshold. **Dashboard API endpoint:** Call `rivian-ls status --offline --format json` from a server-side handler. Use a cron job (`0 * * * *`) to keep the cache fresh. **Keep cache fresh (cron):** ```bash 0 * * * * /usr/local/bin/rivian-ls status --format json > /dev/null 2>&1 ``` ## Field Reference See `references/api-fields.md` for the full JSON schema, all field descriptions, and known limitations.

path: README.md
content: # rivian-ls — OpenClaw Skill

An [OpenClaw](https://openclaw.ai) agent skill for accessing Rivian vehicle telemetry using the [`rivian-ls`](https://github.com/pfrederiksen/rivian-ls) CLI.

Get battery level, range, charge state, lock status, door/frunk state, tire pressures, cabin temp, and location — from your terminal, your dashboard, or any automation.

> ⚠️ Uses an unofficial Rivian API. Not affiliated with Rivian Automotive.

## What It Does

- **Status summaries** — battery %, range, charge state, odometer, ready score
- **Security checks** — lock state, door/frunk/liftgate status
- **Telemetry** — cabin temp, tire pressures, GPS location
- **Dashboard integration** — JSON API endpoint pattern for live panels
- **Automation** — cron-friendly cache refresh, alert triggers

## Installation

### Install the skill via ClawHub

```bash
clawhub install rivian-ls
```

### Install the `rivian-ls` CLI (required)

```bash
# From source (Go 1.21+)
git clone https://github.com/pfrederiksen/rivian-ls.git
cd rivian-ls && make build && cp rivian-ls /usr/local/bin/

# Or Homebrew
brew install pfrederiksen/tap/rivian-ls
```

### Authenticate (two-phase MFA)

Rivian requires MFA. Use the two-phase flow:

```bash
# Phase 1: Send credentials, triggers SMS code
rivian-ls login --email [REDACTED_SECRET] --password secret

# Phase 2: Complete with OTP after receiving SMS (within ~60 seconds)
rivian-ls login --otp 123456
```

Credentials are cached automatically at `~/.config/rivian-ls/credentials.json` and auto-refresh. Subsequent runs use the cache — no re-auth needed unless tokens expire.

## Usage

Once the skill is installed and `rivian-ls` is authenticated, just ask OpenClaw:

- *"What's my Rivian's battery at?"*
- *"Is my truck locked?"*
- *"Show me the Rivian status"*
- *"Are any doors open on the Rivian?"*
- *"What are my tire pressures?"*

## Bundled Script

```bash
# Human-readable summary
python3 scripts/rivian_status.py

# JSON output (for piping, dashboards, etc.)
python3 scripts/rivian_status.py --format json

# Force live fetch from Rivian API
python3 scripts/rivian_status.py --live
```

## Dashboard Integration

Add a live Rivian panel to any web dashboard:

```typescript
// Server-side API endpoint
import { execSync } from 'child_process';

export async function GET() {
  const output = execSync('rivian-ls status --format json --offline');
  return json({ data: JSON.parse(output) });
}
```

Keep the cache fresh with a cron job:

```bash
# /etc/cron.d/rivian-refresh
0 * * * * /usr/local/bin/rivian-ls status --format json > /dev/null 2>&1
```

## Field Reference

See [`references/api-fields.md`](references/api-fields.md) for the full JSON schema.

Key fields:

| Field | Description |
|-------|-------------|
| `BatteryLevel` | State of charge (0–100%) |
| `RangeEstimate` | Estimated range in miles |
| `ChargeState` | "charging", "complete", "disconnected" |
| `IsLocked` | True if all doors locked |
| `CabinTemp` | Interior temperature °F |
| `Doors.*` | Per-door open/closed state |
| `TirePressures.*` | Per-tire PSI + status |
| `ReadyScore` | Rivian's vehicle readiness score |
| `Odometer` | Total miles driven |

## Security

- No credentials hardcoded — uses `rivian-ls` secure credential cache
- Script uses `shell=False`, specific exception types, type hints
- 0/61 VirusTotal detections

## License

MIT


path: references/api-fields.md
content: # rivian-ls JSON Field Reference

All fields returned by `rivian-ls status --format json`:

| Field | Type | Description |
|-------|------|-------------|
| VehicleID | string | Internal vehicle identifier |
| VIN | string | Vehicle Identification Number |
| Name | string | User-set vehicle name |
| Model | string | Rivian model (e.g. "R1T", "R1S") |
| UpdatedAt | ISO 8601 | Timestamp of last data fetch |
| BatteryLevel | float | State of charge (0–100%) |
| BatteryCapacity | float | Usable capacity in kWh |
| RangeEstimate | float | Estimated range in miles |
| ChargeState | string | "charging", "complete", "disconnected" |
| ChargeLimit | int | Target charge limit (%) |
| ChargingRate | float\|null | Current charge rate in kW (null if not charging) |
| TimeToCharge | string\|null | Human-readable time to charge limit |
| Location.Latitude | float | GPS latitude |
| Location.Longitude | float | GPS longitude |
| Location.UpdatedAt | ISO 8601 | Timestamp of last location update |
| CabinTemp | float\|null | Interior temperature in °F |
| ExteriorTemp | float\|null | Outside temperature in °F |
| IsLocked | bool | True if all doors locked |
| IsOnline | bool | True if vehicle is connected |
| Odometer | float | Total miles driven |
| Doors.FrontLeft | string | "closed" or "open" |
| Doors.FrontRight | string | "closed" or "open" |
| Doors.RearLeft | string | "closed" or "open" |
| Doors.RearRight | string | "closed" or "open" |
| Windows.FrontLeft | string | "closed" or "open" |
| Windows.FrontRight | string | "closed" or "open" |
| Windows.RearLeft | string | "closed" or "open" |
| Windows.RearRight | string | "closed" or "open" |
| Frunk | string | "closed", "open", or "unknown" |
| Liftgate | string | "closed", "open", or "unknown" |
| TonneauCover | string | "closed", "open", or "unknown" (R1T bed cover) |
| TirePressures.FrontLeft | float | PSI (0 if unavailable) |
| TirePressures.FrontRight | float | PSI |
| TirePressures.RearLeft | float | PSI |
| TirePressures.RearRight | float | PSI |
| TirePressures.FrontLeftStatus | string | "OK" or warning string |
| TirePressures.FrontRightStatus | string | "OK" or warning string |
| TirePressures.RearLeftStatus | string | "OK" or warning string |
| TirePressures.RearRightStatus | string | "OK" or warning string |
| ReadyScore | float | Rivian's computed vehicle readiness (0–100%) |
| RangeStatus | string | "normal", "low", or "critical" |

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Authentication failure |
| 2 | Vehicle not found |
| 3 | API error / network failure |
| 4 | Invalid arguments |

## Known Limitations

- Tire PSI fields return 0 when the vehicle hasn't recently provided pressure data
- Liftgate and TonneauCover may return "unknown" depending on vehicle trim
- WebSocket subscriptions may fail; rivian-ls auto-falls back to polling
- This uses an **unofficial** Rivian API — may break without notice


path: scripts/rivian_status.py
content: #!/usr/bin/env python3
"""
Fetch Rivian vehicle telemetry via rivian-ls CLI and return structured data.

Usage:
    python3 rivian_status.py [--live] [--format text|json]

Options:
    --live      Fetch fresh data from Rivian API (requires cached credentials)
    --format    Output format: 'text' (default) or 'json'

Exits:
    0  Success
    1  rivian-ls not found or not authenticated
    2  No data available
"""

import argparse
import json
import subprocess
import sys
from typing import Optional


def run_rivian_ls(live: bool = False) -> Optional[dict]:
    """Run rivian-ls and return parsed JSON data."""
    cmd = ["rivian-ls", "status", "--format", "json"]
    if not live:
        cmd.append("--offline")
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=20,
            shell=False,
        )
        if result.returncode != 0:
            print(f"Error: rivian-ls exited {result.returncode}: {result.stderr.strip()}", file=sys.stderr)
            return None
        return json.loads(result.stdout)
    except FileNotFoundError:
        print("Error: rivian-ls not found. Install with: go install github.com/pfrederiksen/rivian-ls@latest", file=sys.stderr)
        sys.exit(1)
    except subprocess.TimeoutExpired:
        print("Error: rivian-ls timed out.", file=sys.stderr)
        return None
    except json.JSONDecodeError as e:
        print(f"Error: Failed to parse rivian-ls output: {e}", file=sys.stderr)
        return None


def format_text(data: dict) -> str:
    """Format vehicle data as human-readable text."""
    lines = []
    lines.append(f"🚙 Rivian  |  {'● Online' if data.get('IsOnline') else '○ Offline'}")
    lines.append(f"Battery:  {data.get('BatteryLevel', 0):.1f}%  |  Range: {data.get('RangeEstimate', 0):.0f} mi")
    lines.append(f"Charge:   {data.get('ChargeState', '—')}  (limit: {data.get('ChargeLimit', '—')}%)")
    if data.get('ChargingRate'):
        lines.append(f"          ⚡ {data['ChargingRate']} kW charging")
    lines.append(f"Locked:   {'Yes 🔒' if data.get('IsLocked') else 'No 🔓'}")
    lines.append(f"Cabin:    {data.get('CabinTemp', 'N/A')}°F")
    lines.append(f"Odometer: {data.get('Odometer', 0):,.0f} mi")
    lines.append(f"Ready:    {data.get('ReadyScore', 0):.0f}%")

    doors = data.get('Doors', {})
    if doors:
        door_states = [
            f"FL:{doors.get('FrontLeft','?')}",
            f"FR:{doors.get('FrontRight','?')}",
            f"RL:{doors.get('RearLeft','?')}",
            f"RR:{doors.get('RearRight','?')}",
        ]
        lines.append(f"Doors:    {' | '.join(door_states)}")

    frunk = data.get('Frunk', 'unknown')
    liftgate = data.get('Liftgate', 'unknown')
    lines.append(f"Frunk: {frunk}  |  Liftgate: {liftgate}")

    tires = data.get('TirePressures', {})
    if tires and tires.get('FrontLeft', 0) > 0:
        lines.append(
            f"Tires:    FL:{tires['FrontLeft']} FR:{tires['FrontRight']} "
            f"RL:{tires['RearLeft']} RR:{tires['RearRight']} PSI"
        )

    loc = data.get('Location')
    if loc and loc.get('Latitude'):
        lines.append(f"Location: {loc['Latitude']:.4f}, {loc['Longitude']:.4f}")

    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(description="Fetch Rivian vehicle telemetry")
    parser.add_argument("--live", action="store_true", help="Fetch live data (default: offline cache)")
    parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format")
    args = parser.parse_args()

    data = run_rivian_ls(live=args.live)
    if not data:
        sys.exit(2)

    if args.format == "json":
        print(json.dumps(data, indent=2))
    else:
        print(format_text(data))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)