44r0n7 29e53d16b0 feat: complete daemon core milestone
Finish Milestone 3 with persisted config, socket IPC, registry CRUD,
periodic discovery, manual add, and app-cache refresh support.
2026-04-14 10:19:14 -04:00
2026-04-14 09:02:32 -04:00
2026-04-14 10:19:14 -04:00
2026-04-14 09:02:32 -04:00
2026-04-14 09:02:32 -04:00
2026-04-14 09:02:32 -04:00
2026-04-14 09:02:32 -04:00

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:

{ "ok": true, "data": { ... } }

Errors:

{
  "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

# 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

{
  "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

{
  "device_id": "a3f2c1d4-...",
  "power": "on",
  "active_app": {
    "id": "12",
    "name": "Netflix"
  },
  "volume": {
    "level": 42,
    "muted": false
  },
  "timestamp": "2026-04-14T11:05:00Z"
}

App

{
  "id": "12",
  "name": "Netflix",
  "version": "4.1.218",
  "platform_id": "12"
}

Configuration

Located at ~/.config/tvctl/config.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

# 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

S
Description
No description provided
Readme 781 KiB
Languages
Rust 100%