pyadcp
Asyncio-native Python library for Sony projectors' ADCP protocol. Built as the backend for a Home Assistant integration; covers the VW and XW series and every other model that speaks ADCP.
pyadcp talks to Sony's Advanced Display Control Protocol, the text-based network control interface on Sony VPL home cinema projectors from 2016 onward. It handles the authentication handshake, command framing and error mapping, and exposes typed getters and setters for power, input, picture settings, HDR, lamp control, status, warnings, timers and remote key emulation. Capabilities are discovered from the projector at runtime, so option lists and numeric ranges match the connected model without per-model tables. Written clean-room from Sony's published protocol manuals. MIT licensed.
Install
pip install pyadcp
Python 3.12 or newer. No runtime dependencies.
Usage
import asyncio
from pyadcp import Projector
async def main() -> None:
async with Projector("192.168.1.50", password="Projector") as projector:
await projector.power_on()
status = await projector.poll_status()
print(status.power_status, status.modelname, status.signal)
await projector.set_input("hdmi1")
await projector.set_picture_mode("reference")
await projector.set_contrast(55)
await projector.send_key("menu")
# Ask the projector what it supports instead of hard-coding a model.
caps = await projector.capabilities()
print(caps.options("picture_mode")) # ('cinema_film1', 'reference', ...)
print(caps.range("contrast")) # NumericRange(min=0, max=100, step=1)
print(caps.supports("lamp_control")) # True on lamp models, False on laser
print(caps.options("key")) # the remote key codes this unit accepts
print(caps.unsupported) # commands this unit answered err_cmd to
asyncio.run(main())
poll_status() is what a polling integration calls on every refresh. It reads
power state, errors, warnings, timers and the input signal, and fills in the
model name, serial number, MAC address and firmware versions from a cached
identify() so a routine poll is five round trips. A full capabilities()
discovery is about 200 round trips and runs once; it is cached, re-probes
anything the projector refused while in standby on the next call, and takes
names=[...] to limit it to the commands you care about.
For building a client on top of the library there are a few more pieces:
per-suffix options and ranges (caps.range("blanking", "top")),
caps.to_dict() / Capabilities.from_dict() plus
projector.restore_capabilities() to persist discovery across restarts,
new.changes_since(old) to learn what to update after a re-probe, and
projector.discover_capabilities() (or capabilities(progress=...)) for
per-command progress. docs/integration-guide.md describes them, together
with what the library deliberately leaves to the client and why.
Lower levels are available when you need them:
from pyadcp import AdcpConnection, get_command
async with AdcpConnection("192.168.1.50") as conn:
print(await conn.send(get_command("power_status").query())) # 'standby'
print(await conn.send('timer ?')) # [{'operation': 3400}, ...]
Check whether a host speaks ADCP, and whether it needs a password, without opening a session or sending a command:
from pyadcp import ProbeOutcome, probe
result = await probe("192.168.1.50") # reads the greeting only
print(result.outcome, result.is_adcp) # ProbeOutcome.AUTH_REQUIRED True
result = await probe("192.168.1.50", password="Projector")
if result.outcome is ProbeOutcome.AUTH_FAILED: # ADCP, wrong password
...
Expected outcomes (refused, unreachable, timed out, closed, not ADCP, host
denied, no auth, auth required, password accepted, rejected or unconfirmed)
come back on the result rather than as exceptions; a rejected password is
always reported as ADCP. The default timeout is 15 s because a unit in
low-power standby can be slow to answer. docs/integration-guide.md lists
every outcome and how a config flow should route on them.
Find projectors on the local network through their SDAP broadcasts:
from pyadcp import discover
for packet in await discover(timeout=35):
print(packet.product_name, packet.serial, packet.address)
Command line
A small helper is included for trying things against a real unit:
python -m pyadcp status 192.168.1.50
python -m pyadcp capabilities 192.168.1.50
python -m pyadcp -v send 192.168.1.50 'picture_mode ?' 'contrast ? --range'
python -m pyadcp discover
python -m pyadcp probe 192.168.1.50 --password Projector
--password (or PYADCP_PASSWORD), --port, --timeout and
--wake-timeout apply to the first three; -v logs every line exchanged
with a timestamp. send exits with status 1 if any line was answered with an
error. probe prints its result as JSON, checks a password only when one is
given (it does not assume the factory value), and exits with status 0 only
for no_auth, auth_required and auth_ok. discover prints [] and a
note on stderr about the Advertisement service when nothing answered.
Errors
Every err_* reply becomes a typed exception: CommandFormatError
(unsupported or malformed command), CommandOptionError, CommandInactiveError
(for example a picture setting while in standby), CommandValueError,
Internal1Error and Internal2Error. Transport problems raise
AdcpConnectionError, AuthenticationError or AdcpTimeoutError. All of them
derive from AdcpError, which is the one to catch in a polling loop. A
connection lost after a set command was written is reported rather than
retried, because the projector may already have acted on it; queries are
resent once. A mistake on the caller's side (unknown command name, wrong value
type, bad suffix) raises UnknownCommandError or CommandFormError before
anything is sent.
Projector setup
ADCP is on by default with authentication enabled and the password
Projector, the same as the web administrator password. Two web-page
settings are involved: the ADCP page turns authentication on or off, and
once it is on the password checked is the Administrator (root)
password from the Password page. The separate User account there is
not used by ADCP, and ADCP takes no user name. A client should
probe() first: NO_AUTH means no password is needed, AUTH_REQUIRED
means ask for one and probe again, AUTH_FAILED means ask again. At
runtime a failed handshake raises AuthenticationError, which is never
retried; docs/integration-guide.md describes the re-authentication flow.
To control the projector while it is in standby, set "Standby mode" to
Standard or "Network management" to On in the projector menu; with it off
the unit leaves the network as it reaches standby and only a Wake-on-LAN
packet (with "Remote start" On) brings it back, which the library does not
send. Discovery
needs the Advertisement service (on by default) and a client on the same
subnet; PJ Talk and DDDP can be off. docs/integration-guide.md lists
what a client should tell its users about these settings.
Picture settings (picture_mode, contrast, aspect, hdr, ...) answer
err_inactive not only in standby but also while the projector is on with
no input signal, so a CommandInactiveError on one of them usually means
"no source", not a fault. docs/hardware-checklist.md records what a
VPL-VW295ES confirmed.
Development
uv sync
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run pytest
Tests run against tests/fake_projector.py, an asyncio server that speaks ADCP
as the manuals describe it, so CI needs no hardware. docs/protocol.md is a
derived description of the protocol.
Releasing
Releases are published to PyPI by .github/workflows/publish.yml through
trusted publishing, so no API
token is stored anywhere. To release:
- Set the new version in
pyproject.tomlandsrc/pyadcp/__init__.py(a test checks they agree), runuv lock, and merge that change. - Tag the merge commit
vX.Y.Zand push the tag. The workflow refuses a tag that does not match the project version. - Running the workflow by hand from the Actions tab publishes the current commit to TestPyPI instead, for checking the page before a real release.
The PyPI project must trust this repository's publish.yml with the pypi
environment (and TestPyPI the testpypi environment); that is set once on
each index's "Publishing" page by the project owner.
Metadata
Release files for pyadcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyadcp-0.1.0.tar.gz | 103.6 kB | With provenance | Details for pyadcp-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyadcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pyadcp-0.1.0-py3-none-any.whl |
Total release size: 148.5 kB
Release files / pyadcp-0.1.0.tar.gz
| Download URL | pyadcp-0.1.0.tar.gz |
|---|---|
| Size | 103.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9d7388a89e21cf81f2390afaba3fd567a197530ec56de2c1a2125fba5b4c093
|
|
BLAKE2b-256 checksum How to use checksums |
5b17c8cfede1b3a3c4079f9a5997b2b776e158a1e606148b8192437cdb262215
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / pyadcp-0.1.0-py3-none-any.whl
| Download URL | pyadcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 44.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f539ff7c8d5c7dfc46997782743551ef78bbad76f4427ec082cad71c217fd3ec
|
|
BLAKE2b-256 checksum How to use checksums |
12f5775903d46963a3487459f29a85866b12dccfa57ca5e9c55139783505af8d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for pyadcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pyadcp-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.