Skip to main content

pyadcp

CI

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:

  1. Set the new version in pyproject.toml and src/pyadcp/__init__.py (a test checks they agree), run uv lock, and merge that change.
  2. Tag the merge commit vX.Y.Z and push the tag. The workflow refuses a tag that does not match the project version.
  3. 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)

Source distribution for pyadcp 0.1.0
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)

Table of built distributions (wheels) for pyadcp 0.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page