{"id":51840164,"url":"https://github.com/hugoh/pyatv-http","last_synced_at":"2026-07-23T03:30:54.532Z","repository":{"id":372562548,"uuid":"1308253032","full_name":"hugoh/pyatv-http","owner":"hugoh","description":"HTTP interface for controlling Apple TVs via pyatv, with bearer-token auth","archived":false,"fork":false,"pushed_at":"2026-07-22T01:58:54.000Z","size":119,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-22T03:24:14.292Z","etag":null,"topics":["apple-tv","fastapi","home-automation","http-api","pyatv","python","rest-api"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/pyatv-http/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hugoh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-22T01:07:54.000Z","updated_at":"2026-07-22T02:02:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hugoh/pyatv-http","commit_stats":null,"previous_names":["hugoh/pyatv-http"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/hugoh/pyatv-http","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hugoh%2Fpyatv-http","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hugoh%2Fpyatv-http/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hugoh%2Fpyatv-http/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hugoh%2Fpyatv-http/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hugoh","download_url":"https://codeload.github.com/hugoh/pyatv-http/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hugoh%2Fpyatv-http/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35787350,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-23T02:00:06.683Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apple-tv","fastapi","home-automation","http-api","pyatv","python","rest-api"],"created_at":"2026-07-23T03:30:53.519Z","updated_at":"2026-07-23T03:30:54.525Z","avatar_url":"https://github.com/hugoh.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyatv-http\n\nAn HTTP interface for controlling Apple TVs, built on top of\n[pyatv](https://github.com/postlund/pyatv).\n\n## Features\n\n- `GET /\u003cname\u003e/power-state` — reads the current power state without changing it.\n- `PUT /\u003cname\u003e/power-state` (body `{\"power_state\": \"on\"}` or `{\"power_state\":\n  \"off\"}`) — checks the Apple TV's current power state and only sends a\n  command if it differs from the desired state. `POST` is also accepted as an\n  identical alias, for clients/platforms that can't issue `PUT` requests.\n- `GET /devices` — lists the devices available in the config file.\n- `GET /health` — unauthenticated liveness check, for load balancers/uptime\n  monitors.\n- Every other request requires a bearer token, configured as a list of\n  accepted tokens in the config file.\n- Config-driven: one TOML file lists the port to listen on, accepted API\n  tokens, and the paired devices.\n- Pairing stays out-of-band, via pyatv's own `atvremote` CLI; a `pyatv-http gen-config`\n  helper turns a paired device's stored credentials into a config snippet.\n\n## Setup\n\n### 1. Find your Apple TV\n\n```sh\nuvx --from pyatv atvremote scan\n```\n\nThis lists every Apple TV on the network along with its identifiers, IP\naddress, and which protocols it supports (AirPlay, Companion, etc).\n\n### 2. Pair with `atvremote`\n\nPower control uses the **Companion** protocol, and pyatv-http talks to the\ndevice over **AirPlay** for the initial handshake, so pair both:\n\n```sh\nuvx --from pyatv atvremote --id \u003cdevice-identifier\u003e pair --protocol airplay\nuvx --from pyatv atvremote --id \u003cdevice-identifier\u003e pair --protocol companion\n```\n\nFollow the on-screen PIN prompt for each. Credentials are stored in pyatv's\nstorage file (default `~/.pyatv.conf`).\n\n### 3. Generate a config entry\n\n```sh\nuv run pyatv-http gen-config \\\n  --identifier \u003cdevice-identifier\u003e \\\n  --address \u003cdevice-ip-or-hostname\u003e \\\n  --key living_room\n```\n\n`\u003cdevice-identifier\u003e` and `\u003cdevice-ip-or-hostname\u003e` come from the `scan`\noutput in step 1; `--key` is whatever short name you want to use in URLs\n(`--name` sets the human-readable display name too, defaulting to `--key`).\n\nThis prints a `[devices.living_room]` TOML block built from the stored\npairing. Paste it into your config file (see below). If the identifier\ndoesn't match any paired device, the command lists the identifiers it does\nhave on file.\n\n### 4. Write the config file\n\n```toml\nport = 8080\n\n[auth]\ntokens = [\"a-long-random-token\"]\n\n[devices.living_room]\nname = \"Living Room\"\nidentifier = \"AA:BB:CC:DD:EE:FF\"\naddress = \"10.0.0.5\"\n\n[devices.living_room.protocols.airplay]\nidentifier = \"AA:BB:CC:DD:EE:FF\"\ncredentials = \"...\"\n\n[devices.living_room.protocols.companion]\nidentifier = \"11:22:33:44:55:66\"\ncredentials = \"...\"\n```\n\n- `port` — port the HTTP server listens on. Optional, defaults to `8080`.\n- `[auth].tokens` — required, non-empty list of bearer tokens accepted on every\n  request (see [API](#api) below). Generate one with e.g.\n  `python3 -c \"import secrets; print(secrets.token_urlsafe(32))\"`.\n- The `[devices.\u003ckey\u003e]` table key (`living_room` above) is the URL path\n  segment used in requests, e.g. `PUT /living_room/power-state`.\n- `identifier` — the device's main pyatv identifier (from `atvremote scan`).\n- `address` — the Apple TV's IP address or hostname; used to connect\n  directly instead of relying on mDNS discovery at request time.\n- `[devices.\u003ckey\u003e.protocols.\u003cprotocol\u003e]` — one block per paired protocol,\n  exactly as generated by `gen-config`. Supported protocol names: `airplay`,\n  `companion`, `dmap`, `mrp`, `raop`.\n\n### 5. Run the server\n\n```sh\nuv run pyatv-http serve --config config.toml\n```\n\n`--config` is optional; if omitted, it defaults to\n`$XDG_CONFIG_HOME/pyatv-http/config.toml`, falling back to\n`~/.config/pyatv-http/config.toml` when `$XDG_CONFIG_HOME` isn't set.\n\nBy default the server binds `0.0.0.0`; pass `--host` to bind a specific\ninterface.\n\n## API\n\nEvery request must include one of the configured tokens as a bearer token:\n\n```sh\nTOKEN=a-long-random-token\n\ncurl -X PUT http://localhost:8080/living_room/power-state \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"power_state\": \"on\"}'\n\ncurl -X PUT http://localhost:8080/living_room/power-state \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"power_state\": \"off\"}'\n\n# POST is also accepted, identical to PUT, for clients that can only\n# issue GET/POST requests:\ncurl -X POST http://localhost:8080/living_room/power-state \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"power_state\": \"on\"}'\n\ncurl http://localhost:8080/living_room/power-state \\\n  -H \"Authorization: Bearer $TOKEN\"\n\ncurl http://localhost:8080/devices \\\n  -H \"Authorization: Bearer $TOKEN\"\n\ncurl http://localhost:8080/health\n\n# Fallback for clients that can't set an Authorization header (see\n# \"Limited HTTP clients\" below): access_token in the query string for GET,\n# access_token as a JSON body field for POST. Not available for PUT.\ncurl \"http://localhost:8080/living_room/power-state?access_token=$TOKEN\"\n\ncurl -X POST http://localhost:8080/living_room/power-state \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"power_state\": \"on\", \"access_token\": \"'\"$TOKEN\"'\"}'\n```\n\n`GET`/`PUT`/`POST /\u003cname\u003e/power-state` all return a JSON body:\n`{\"device\": \"living_room\", \"power_state\": \"on\"}`.\n\n`GET /devices` returns the devices available in the config file:\n\n```json\n[{\"device\": \"living_room\", \"name\": \"Living Room\"}]\n```\n\n`GET /health` (no token required) returns `{\"status\": \"ok\"}`.\n\n| Status | Meaning                                                           |\n| ------ | ------------------------------------------------------------------ |\n| 200    | Command sent (or no-op, if the device was already in that state)   |\n| 401    | Missing or invalid bearer token                                    |\n| 404    | No device configured under that name                               |\n| 504    | Device could not be found/reached on the network                   |\n| 502    | pyatv raised an error while connecting or sending the command      |\n\n### Interactive API docs\n\nFastAPI auto-generates interactive documentation for the running server:\n\n- `GET /docs` — Swagger UI (use the \"Authorize\" button to set your bearer\n  token, then try requests directly from the browser).\n- `GET /redoc` — ReDoc view of the same schema.\n- `GET /openapi.json` — the raw OpenAPI schema.\n\nThese three routes are not themselves behind the bearer-token check.\n\n### Limited HTTP clients (e.g. Hubitat Rule Machine)\n\nA lot of home-automation \"rule engine\" style integrations — Hubitat's Rule\nMachine is the one that prompted this — can only fire `GET` and `POST`\nrequests, and can't attach a custom `Authorization` header to them. Two\nthings in this API exist specifically to accommodate that:\n\n1. **`POST` is a full alias for `PUT`** on `/\u003cname\u003e/power-state`. State\n   changes normally belong on `PUT` (it's idempotent and semantically\n   correct — \"set this resource to this value\"), but any client that can\n   only do `GET`/`POST` can use `POST` with the exact same body and get\n   identical behavior.\n2. **The bearer token can be passed in-band instead of as a header**, as a\n   fallback that's only checked when no valid `Authorization` header is\n   present. This follows [RFC 6750](https://www.rfc-editor.org/rfc/rfc6750)\n   (OAuth 2.0 Bearer Token Usage) sections 2.2 and 2.3, which define\n   `access_token` as the parameter name for exactly this case:\n   - `GET` requests: `?access_token=...` query parameter.\n   - `POST` requests: `\"access_token\"` field in the JSON body, alongside\n     `power_state`.\n\n   This fallback is **deliberately not available on `PUT`** — `PUT` stays\n   the strict, header-only, \"do it the correct way\" method. If your client\n   can set a custom header, prefer `PUT` with an `Authorization` header;\n   the fallback exists only for clients that genuinely can't.\n\n**Security note:** a token in a URL or a JSON body is more likely to end up\nsomewhere you don't want it — server access logs, browser history, an\nintermediate proxy's logs — than one in an `Authorization` header. Only rely\non this fallback when pyatv-http is reachable exclusively on a trusted local\nnetwork (the normal setup for a Hubitat hub talking to a LAN service, not\nsomething exposed to the internet), and consider using a token dedicated to\nthat integration so it can be rotated on its own if it ever leaks.\n\n## Notes\n\n- Each request opens a fresh connection to the Apple TV, checks its current\n  power state, and only sends the power command if it differs from the\n  desired state — there's no persistent connection or background polling.\n- Pairing is entirely out-of-band via `atvremote`; this project never\n  performs the pairing handshake itself.\n\n## Development\n\n```sh\nuv sync\nuv run pytest   # or: mise run test\nhk check --all\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhugoh%2Fpyatv-http","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhugoh%2Fpyatv-http","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhugoh%2Fpyatv-http/lists"}