chore: scaffold tvctl foundation
Set up the Rust crate, baseline module layout, and project docs so the repository matches the design bundle and builds cleanly as a starting point.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
# tvctl
|
||||
|
||||
> A local-first daemon and CLI that provides a unified, brand-agnostic control layer for smart TVs —
|
||||
> giving technical users and tool builders a stable, scriptable API without caring what's connected.
|
||||
|
||||
---
|
||||
|
||||
## What is tvctl?
|
||||
|
||||
`tvctl` is a self-hosted control plane for smart TVs. It runs as a background daemon (`tvctld`) on your
|
||||
Linux machine and exposes two interfaces:
|
||||
|
||||
- A **CLI** (`tvctl`) for direct control, scripting, and exploration
|
||||
- A **local HTTP API** (`/v1/...`) for tool builders and automation
|
||||
|
||||
The goal is simple: make your TV fully scriptable and explorable from the command line, without cloud
|
||||
dependencies, without proprietary apps, and without caring what brand of TV you own.
|
||||
|
||||
If you've ever wanted to `curl` your TV, write a script that launches an app, automate input switching,
|
||||
or build your own dashboard — `tvctl` is the foundation that makes that possible.
|
||||
|
||||
---
|
||||
|
||||
## Who is this for?
|
||||
|
||||
**Homelab operators** who want their TV under the same level of control as the rest of their infrastructure.
|
||||
|
||||
**Tool builders** who want to build TV-adjacent tooling (stream deck plugins, Home Assistant integrations,
|
||||
automation scripts) without reinventing device discovery and platform APIs from scratch.
|
||||
|
||||
**Curious technical users** who want to explore what their TV can actually do.
|
||||
|
||||
`tvctl` is not for average TV users. If you're comfortable with a terminal and you've ever felt limited
|
||||
by your TV's remote, you're the target audience.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ tvctld │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ Unix Socket │ │ HTTP API (:7272) │ │
|
||||
│ │ (CLI only) │ │ /v1/... (tool builders) │ │
|
||||
│ └──────┬──────┘ └────────────┬─────────────┘ │
|
||||
│ │ │ │
|
||||
│ └──────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼──────────┐ │
|
||||
│ │ Core Router │ │
|
||||
│ └─────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────┼──────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌──▼───┐ ┌─────▼────┐ ┌────▼────┐ │
|
||||
│ │Device│ │ App │ │Discovery│ │
|
||||
│ │Registry │ Cache │ │Service │ │
|
||||
│ └──────┘ └──────────┘ └─────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼──────────┐ │
|
||||
│ │ Adapter Registry │ │
|
||||
│ └─────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────┴──────────┐ │
|
||||
│ │ │ │
|
||||
│ ┌────▼────┐ ┌─────▼──────┐ │
|
||||
│ │ Roku │ │ Google TV │ ← future │
|
||||
│ │ Adapter │ │ Adapter │ │
|
||||
│ │ (ECP) │ │ (ADB) │ │
|
||||
│ └─────────┘ └────────────┘ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
**`tvctld` — The Daemon**
|
||||
The heart of the system. Runs as a persistent background service (systemd user unit). Owns device
|
||||
discovery, the device registry, app cache, and state cache. All CLI commands and API requests go
|
||||
through the daemon.
|
||||
|
||||
**Unix Socket**
|
||||
The CLI communicates with the daemon exclusively via a Unix socket (`~/.run/user/1000/tvctl.sock`).
|
||||
Fast, secure, no port conflicts. The socket is only accessible to the running user.
|
||||
|
||||
**HTTP API**
|
||||
The daemon optionally exposes a local HTTP API on `127.0.0.1:7272`. This is the interface for tool
|
||||
builders — Home Assistant integrations, stream deck plugins, scripts, dashboards. Loopback-only by
|
||||
default. All responses are JSON.
|
||||
|
||||
**Adapter Layer**
|
||||
Each TV platform is implemented as an adapter behind a common Rust trait. The adapter handles all
|
||||
platform-specific protocol details. Above the adapter layer, nothing knows or cares whether the TV
|
||||
is a Roku, Google TV, or anything else. Adding a new platform means implementing the adapter trait —
|
||||
nothing else changes.
|
||||
|
||||
**Device Registry**
|
||||
Persisted to `~/.local/share/tvctl/devices.json`. Survives daemon restarts. Devices are discovered
|
||||
once and remembered forever (or until removed). Each device has a UUID, a friendly name, and a
|
||||
platform tag that maps it to an adapter.
|
||||
|
||||
**App Cache**
|
||||
Per-platform, persisted to `~/.local/share/tvctl/cache/{platform}.apps.json`. Grows organically
|
||||
from live TV data — no pre-populated database. The first time you launch an app by name, tvctld
|
||||
fetches the full app list from the TV, caches it, and never fetches it again unless you ask it to.
|
||||
App IDs are stable on all supported platforms.
|
||||
|
||||
**State Cache**
|
||||
In-memory only. Last known state per device. Includes power status, active app, and volume. Always
|
||||
includes a timestamp so callers know how fresh the data is. Cleared on daemon restart.
|
||||
|
||||
---
|
||||
|
||||
## CLI Reference
|
||||
|
||||
`tvctl` uses a consistent **resource-verb** pattern throughout:
|
||||
|
||||
```
|
||||
tvctl <resource> <verb> [args] [flags]
|
||||
```
|
||||
|
||||
Global flags available on every command:
|
||||
|
||||
```
|
||||
--device <name|id> Target a specific device (overrides default)
|
||||
--json Output as JSON for scripting
|
||||
--help Show help for any command
|
||||
--version Show tvctl version
|
||||
```
|
||||
|
||||
### daemon
|
||||
|
||||
Manage the tvctld background service.
|
||||
|
||||
```
|
||||
tvctl daemon start Start the daemon
|
||||
tvctl daemon stop Stop the daemon
|
||||
tvctl daemon restart Restart the daemon
|
||||
tvctl daemon status Show daemon status
|
||||
tvctl daemon install Generate and enable systemd user service
|
||||
tvctl daemon uninstall Remove systemd user service
|
||||
```
|
||||
|
||||
### device
|
||||
|
||||
Discover and manage connected TVs.
|
||||
|
||||
```
|
||||
tvctl device list List all known devices
|
||||
tvctl device discover Scan network for TVs (SSDP)
|
||||
tvctl device add --address <ip> Manually add a device
|
||||
tvctl device select <name|id> Set default device
|
||||
tvctl device info <name|id> Show device details
|
||||
tvctl device remove <name|id> Remove from registry
|
||||
```
|
||||
|
||||
### app
|
||||
|
||||
Manage and launch TV applications.
|
||||
|
||||
```
|
||||
tvctl app list List installed apps on current device
|
||||
tvctl app launch <name|id> Launch an app by name or platform ID
|
||||
tvctl app stop Stop the current app
|
||||
tvctl app refresh Refresh app cache from TV
|
||||
```
|
||||
|
||||
### remote
|
||||
|
||||
Send input to the TV.
|
||||
|
||||
```
|
||||
tvctl remote key <key> Send a single keypress
|
||||
tvctl remote sequence <key> [key...] Send a sequence of keypresses
|
||||
```
|
||||
|
||||
**Available keys:**
|
||||
|
||||
| Category | Keys |
|
||||
|------------|---------------------------------------------------|
|
||||
| Navigation | `home` `back` `up` `down` `left` `right` `select` |
|
||||
| Playback | `play` `pause` `play-pause` `stop` `rewind` `fast-forward` `replay` `skip` |
|
||||
| Channel | `channel-up` `channel-down` |
|
||||
| Volume | `volume-up` `volume-down` `mute` |
|
||||
| Power | `power` `power-on` `power-off` |
|
||||
| Input | `input-hdmi1` `input-hdmi2` `input-hdmi3` `input-hdmi4` `input-av` `input-tuner` |
|
||||
| Other | `search` `info` `options` |
|
||||
|
||||
### state
|
||||
|
||||
Query current device state.
|
||||
|
||||
```
|
||||
tvctl state Show state of default device
|
||||
tvctl state --device <name> Show state of a specific device
|
||||
```
|
||||
|
||||
### dev
|
||||
|
||||
Developer tools. Requires `dev.enabled = true` in config.
|
||||
|
||||
```
|
||||
tvctl dev install <app.zip> Sideload a zip to the TV
|
||||
tvctl dev reload Reload the currently sideloaded app
|
||||
tvctl dev logs Fetch developer logs
|
||||
```
|
||||
|
||||
### config
|
||||
|
||||
View and manage tvctl configuration.
|
||||
|
||||
```
|
||||
tvctl config list Show all config values
|
||||
tvctl config get <key> Get a specific value
|
||||
tvctl config set <key> <value> Set a config value
|
||||
tvctl config reset Reset to defaults
|
||||
tvctl config reload Hot-reload config into running daemon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP API Reference
|
||||
|
||||
The API mirrors the CLI exactly. If you know the CLI, you know the API.
|
||||
|
||||
Base URL: `http://127.0.0.1:7272/v1`
|
||||
|
||||
All requests and responses are JSON. All responses use a consistent envelope:
|
||||
|
||||
```json
|
||||
{ "ok": true, "data": { ... } }
|
||||
```
|
||||
|
||||
Errors:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"error": {
|
||||
"code": "device_not_found",
|
||||
"message": "No device with id 'bedroom' exists.",
|
||||
"hint": "Run tvctl device list to see known devices."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`code` is stable and machine-readable. `hint` is human-readable and may change between versions.
|
||||
Tool builders should match on `code`, never on `hint` or `message`.
|
||||
|
||||
### Endpoints
|
||||
|
||||
```
|
||||
GET /v1/devices List all devices
|
||||
POST /v1/devices/discover Trigger discovery
|
||||
GET /v1/devices/{id} Device info
|
||||
DELETE /v1/devices/{id} Remove device
|
||||
GET /v1/devices/{id}/state Device state
|
||||
GET /v1/devices/{id}/apps List installed apps
|
||||
POST /v1/devices/{id}/apps/launch Launch app
|
||||
POST /v1/devices/{id}/apps/stop Stop current app
|
||||
POST /v1/devices/{id}/apps/refresh Refresh app cache
|
||||
POST /v1/devices/{id}/remote/key Send keypress
|
||||
POST /v1/devices/{id}/remote/sequence Send key sequence
|
||||
POST /v1/devices/{id}/dev/install Sideload zip
|
||||
POST /v1/devices/{id}/dev/reload Reload sideloaded app
|
||||
GET /v1/devices/{id}/dev/logs Fetch logs
|
||||
GET /v1/daemon/status Daemon health
|
||||
GET /v1/config Get full config
|
||||
PATCH /v1/config Update config values
|
||||
POST /v1/config/reload Hot reload config
|
||||
```
|
||||
|
||||
Devices can be addressed by UUID or friendly name:
|
||||
|
||||
```
|
||||
GET /v1/devices/a3f2c1d4-.../state
|
||||
GET /v1/devices/living-room/state ← same result
|
||||
```
|
||||
|
||||
### Example Requests
|
||||
|
||||
```bash
|
||||
# List devices
|
||||
curl http://127.0.0.1:7272/v1/devices
|
||||
|
||||
# Launch Netflix on living room TV
|
||||
curl -X POST http://127.0.0.1:7272/v1/devices/living-room/apps/launch \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"app": "netflix"}'
|
||||
|
||||
# Send keypress
|
||||
curl -X POST http://127.0.0.1:7272/v1/devices/living-room/remote/key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key": "home"}'
|
||||
|
||||
# Get state
|
||||
curl http://127.0.0.1:7272/v1/devices/living-room/state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Shapes
|
||||
|
||||
### Device
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "a3f2c1d4-9b3e-4f1a-8c2d-1e5f7a9b3c4d",
|
||||
"name": "Living Room",
|
||||
"original_name": "Living Room Roku",
|
||||
"platform": "roku",
|
||||
"address": "192.168.1.42",
|
||||
"port": 8060,
|
||||
"is_default": true,
|
||||
"discovered_at": "2026-04-14T10:22:00Z",
|
||||
"last_seen": "2026-04-14T11:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### State
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "a3f2c1d4-...",
|
||||
"power": "on",
|
||||
"active_app": {
|
||||
"id": "12",
|
||||
"name": "Netflix"
|
||||
},
|
||||
"volume": {
|
||||
"level": 42,
|
||||
"muted": false
|
||||
},
|
||||
"timestamp": "2026-04-14T11:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### App
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "12",
|
||||
"name": "Netflix",
|
||||
"version": "4.1.218",
|
||||
"platform_id": "12"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Located at `~/.config/tvctl/config.toml`
|
||||
|
||||
```toml
|
||||
[daemon]
|
||||
socket = "/run/user/1000/tvctl.sock"
|
||||
http_enabled = true
|
||||
http_port = 7272
|
||||
http_host = "127.0.0.1"
|
||||
log_level = "info"
|
||||
|
||||
[discovery]
|
||||
auto_discover = true
|
||||
interval_secs = 300
|
||||
timeout_secs = 5
|
||||
|
||||
[devices]
|
||||
default = "living-room"
|
||||
|
||||
[dev]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
### Config Keys
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `daemon.socket` | `/run/user/{uid}/tvctl.sock` | Unix socket path |
|
||||
| `daemon.http_enabled` | `true` | Expose HTTP API |
|
||||
| `daemon.http_port` | `7272` | HTTP API port |
|
||||
| `daemon.http_host` | `127.0.0.1` | Bind address (loopback by default) |
|
||||
| `daemon.log_level` | `info` | debug / info / warn / error |
|
||||
| `discovery.auto_discover` | `true` | Discover devices on daemon start |
|
||||
| `discovery.interval_secs` | `300` | Re-scan interval (0 = disabled) |
|
||||
| `discovery.timeout_secs` | `5` | Per-device timeout |
|
||||
| `devices.default` | `""` | Default device name or UUID |
|
||||
| `dev.enabled` | `true` | Enable dev tooling commands |
|
||||
|
||||
---
|
||||
|
||||
## Storage Layout
|
||||
|
||||
```
|
||||
~/.config/tvctl/
|
||||
└── config.toml
|
||||
|
||||
~/.local/share/tvctl/
|
||||
├── devices.json ← device registry (daemon managed)
|
||||
└── cache/
|
||||
├── roku.apps.json ← organic app cache, grows from live data
|
||||
├── googletv.apps.json
|
||||
└── firetv.apps.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Status | Protocol | Notes |
|
||||
|----------|--------|----------|-------|
|
||||
| Roku | v1 | ECP (HTTP) | Full support |
|
||||
| Google TV | planned | ADB | Post-MVP |
|
||||
| Fire TV | planned | ADB | Post-MVP |
|
||||
| Samsung | investigated | Tizen | Deprioritized — unstable app IDs |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Install (once binary releases exist)
|
||||
# cargo install tvctl
|
||||
|
||||
# Start the daemon
|
||||
tvctl daemon start
|
||||
|
||||
# Or install as a systemd user service
|
||||
tvctl daemon install
|
||||
|
||||
# Discover TVs on your network
|
||||
tvctl device discover
|
||||
|
||||
# Set your default device
|
||||
tvctl device select "Living Room"
|
||||
|
||||
# Launch an app
|
||||
tvctl app launch netflix
|
||||
|
||||
# Send a keypress
|
||||
tvctl remote key home
|
||||
|
||||
# Query state
|
||||
tvctl state
|
||||
|
||||
# Scripting example
|
||||
tvctl state --json | jq '.data.active_app.name'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## For Tool Builders
|
||||
|
||||
`tvctl` is designed to be a stable foundation for other tools. The HTTP API is versioned from day one
|
||||
(`/v1/...`) and the data shapes are intentional contracts.
|
||||
|
||||
If you're building a Home Assistant integration, a stream deck plugin, or any other TV-adjacent tool:
|
||||
|
||||
- Use the HTTP API, not the CLI, for programmatic access
|
||||
- Address devices by UUID for stability, not friendly name
|
||||
- Match on `error.code` for error handling, never on `error.message`
|
||||
- The API is loopback-only by default — document this for your users
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user