Files
tvctl/README.md
T
44r0n7 45620b1ab5 refactor: clean up daemon and CLI duplication
Reduce repeated adapter dispatch, CLI action rendering, and config save
flows while keeping the current Roku behavior and docs aligned with the
known secret-menu limitations.
2026-04-15 15:25:49 -04:00

486 lines
16 KiB
Markdown

# 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]
```
## Known Issues
- Some Roku secret-menu sequences sent through `tvctl remote send` do not reliably open the expected menu, even when the same sequence works on the physical Roku remote.
- Current Roku input delivery uses ECP `keypress/...` requests only. Roku's official ECP docs also document `keydown/...` and `keyup/...`, which likely need investigation for higher-fidelity secret-menu automation.
- Normal navigation and app control work as expected, but Roku secret-menu automation should currently be treated as experimental.
- Tracking issue: [#1 Investigate Roku secret-menu sequence reliability over ECP](https://git.44r0n.cc/44r0n7/tvctl/issues/1)
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 daemon. When the user service is installed, these
commands manage the systemd user service instead of an ad hoc background process.
```
tvctl daemon start Start the daemon or installed user service
tvctl daemon stop Stop the daemon or installed user service
tvctl daemon restart Restart the daemon or installed user service
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
```
`tvctl app refresh --clear` clears the persisted cache for the current platform
before reloading it from the selected device. Use it when cached app names or IDs
look stale, or when removed apps are still showing up in the cache.
### remote
Send input to the TV.
```
tvctl remote send <key> [key...] Send one or more 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
roku_username = ""
roku_password = ""
```
### 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 |
| `dev.roku_username` | `""` | Optional Roku developer username override |
| `dev.roku_password` | `""` | Optional Roku developer password override |
---
## 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 ad hoc
tvctl daemon start
# Or install as a systemd user service
# After install, daemon start/stop/restart manage the 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 send 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