Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,41 @@ jobs:

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0

publish-docs:
needs: release
if: needs.release.outputs.new_tag != ''
timeout-minutes: 5
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
ref: ${{ needs.release.outputs.new_tag }}

- uses: hugoh/gh-workflows/setup@fe2707057b8da8fab2460fc1f0e816561fa8a958 # v1.3.0

- name: Export OpenAPI schema
run: uv run python scripts/export_openapi.py > openapi.json

- name: Render static API docs
run: npx -y @redocly/cli@latest build-docs openapi.json -o public/index.html

- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
with:
enablement: true

- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: public

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ config.toml
# OS
.DS_Store
Thumbs.db

# Generated OpenAPI docs (scripts/export_openapi.py, release workflow)
/openapi.json
/public/
61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ An HTTP interface for controlling Apple TVs, built on top of
"off"}`) — checks the Apple TV's current power state and only sends a
command if it differs from the desired state. `POST` is also accepted as an
identical alias, for clients/platforms that can't issue `PUT` requests.
- `GET /devices` — lists the devices available in the config file.
- `GET /devices` — lists the devices available in the config file. Public, no
token required.
- `GET /health` — unauthenticated liveness check, for load balancers/uptime
monitors.
- Every other request requires a bearer token, configured as a list of
accepted tokens in the config file.
- `GET /status` / `GET /stats` — optional, opt-in public status page (HTML)
and matching JSON endpoint showing recent command activity and
success/error counts. See [Status page](#status-page).
- Every other request (reading/setting power state) requires a bearer token,
configured as a list of accepted tokens in the config file.
- Config-driven: one TOML file lists the port to listen on, accepted API
tokens, and the paired devices.
tokens, the paired devices, and optional status-page settings.
- Pairing stays out-of-band, via pyatv's own `atvremote` CLI; a `pyatv-http gen-config`
helper turns a paired device's stored credentials into a config snippet.

Expand Down Expand Up @@ -70,6 +74,10 @@ port = 8080
[auth]
tokens = ["a-long-random-token"]

[status]
enabled = true
history_size = 100

[devices.living_room]
name = "Living Room"
identifier = "AA:BB:CC:DD:EE:FF"
Expand All @@ -96,6 +104,10 @@ credentials = "..."
- `[devices.<key>.protocols.<protocol>]` — one block per paired protocol,
exactly as generated by `gen-config`. Supported protocol names: `airplay`,
`companion`, `dmap`, `mrp`, `raop`.
- `[status]` — optional, entirely off by default. `enabled` turns on the
public `/status` and `/stats` endpoints (see [Status page](#status-page));
`history_size` caps how many recent commands are kept in memory (default
`100`).

### 5. Run the server

Expand Down Expand Up @@ -137,8 +149,7 @@ curl -X POST http://localhost:8080/living_room/power-state \
curl http://localhost:8080/living_room/power-state \
-H "Authorization: Bearer $TOKEN"

curl http://localhost:8080/devices \
-H "Authorization: Bearer $TOKEN"
curl http://localhost:8080/devices

curl http://localhost:8080/health

Expand All @@ -161,7 +172,7 @@ curl -X POST http://localhost:8080/living_room/power-state \
[{"device": "living_room", "name": "Living Room"}]
```

`GET /health` (no token required) returns `{"status": "ok"}`.
`GET /devices` and `GET /health` require no token.

| Status | Meaning |
| ------ | ------------------------------------------------------------------ |
Expand All @@ -171,6 +182,37 @@ curl -X POST http://localhost:8080/living_room/power-state \
| 504 | Device could not be found/reached on the network |
| 502 | pyatv raised an error while connecting or sending the command |

### Status page

Set `[status] enabled = true` in the config file (see [Write the config
file](#4-write-the-config-file)) to turn on two extra, unauthenticated
endpoints for checking on the service at a glance:

- `GET /status` — an HTML page listing the configured devices, per-device and
total success/error command counts, and the most recent commands (time,
device, command, result, detail).
- `GET /stats` — the same data as JSON, for scripts or monitoring:

```json
{
"totals": { "living_room": { "success": 12, "error": 1 } },
"global_totals": { "success": 12, "error": 1 },
"recent": [
{
"timestamp": "2026-07-26T15:00:00+00:00",
"device": "living_room",
"command": "set_power_state",
"ok": true,
"detail": "on"
}
]
}
```

Both are `404` when `[status].enabled` is left at its default (`false`).
History is kept in memory only (up to `history_size` entries) and resets on
restart — there's no persistence across process restarts.

### Interactive API docs

FastAPI auto-generates interactive documentation for the running server:
Expand All @@ -182,6 +224,11 @@ FastAPI auto-generates interactive documentation for the running server:

These three routes are not themselves behind the bearer-token check.

A static, always-up-to-date copy of the same schema (rendered with
[Redoc](https://github.com/Redocly/redoc)) is published on every release to
[hugoh.github.io/pyatv-http](https://hugoh.github.io/pyatv-http/) — handy for
browsing the API without a server running.

### Limited HTTP clients (e.g. Hubitat Rule Machine)

A lot of home-automation "rule engine" style integrations — Hubitat's Rule
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dynamic = ["version"]

dependencies = [
"fastapi>=0.121.0",
"jinja2>=3.1.6",
"pyatv>=0.18.0",
"uvicorn>=0.38.0",
]
Expand Down
34 changes: 34 additions & 0 deletions scripts/export_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Dump the FastAPI app's OpenAPI schema to stdout, for CI doc generation.

Run with: uv run python scripts/export_openapi.py > openapi.json
"""

import json
import sys

from pyatv_http.app import create_app
from pyatv_http.config import AppConfig, DeviceConfig


def _placeholder_config() -> AppConfig:
device = DeviceConfig(
key="living_room",
name="Living Room",
address="10.0.0.5",
identifier="AA:BB:CC:DD:EE:FF",
)
return AppConfig(
port=8080,
devices={"living_room": device},
auth_tokens=frozenset({"placeholder"}),
status_enabled=True,
)


def main() -> None:
app = create_app(_placeholder_config())
json.dump(app.openapi(), sys.stdout, indent=2)


if __name__ == "__main__":
main()
Loading