# 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 [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 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 Manually add a device tvctl device select Set default device tvctl device info Show device details tvctl device remove Remove from registry ``` ### app Manage and launch TV applications. ``` tvctl app list List installed apps on current device tvctl app launch 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...] 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 Show state of a specific device ``` ### dev Developer tools. Requires `dev.enabled = true` in config. ``` tvctl dev install 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 Get a specific value tvctl config set Set a config value tvctl config reset Reset to defaults tvctl config reload Hot-reload config into running daemon ``` ### completion Generate shell completions to stdout. ```bash tvctl completion bash > ~/.local/share/bash-completion/completions/tvctl tvctl completion zsh > ~/.zfunc/_tvctl tvctl completion fish > ~/.config/fish/completions/tvctl.fish ``` --- ## 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' # Optional shell completions tvctl completion zsh > ~/.zfunc/_tvctl ``` --- ## 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