Skip to main content

Async library to control Denon receivers over RS232

Project description

denon-rs232

Async Python library to control Denon AV receivers over RS232 serial, built on serialx.

Installation

pip install denon-rs232

Requires Python 3.12+.

Quick start

import asyncio
from denon_rs232 import DenonReceiver, InputSource

async def main():
    receiver = DenonReceiver("/dev/ttyUSB0")
    await receiver.connect()
    await receiver.query_state()

    # State is fully populated after query_state()
    print(f"Power: {receiver.state.power}")
    print(f"Volume: {receiver.state.volume} dB")
    print(f"Input: {receiver.state.input_source}")

    # Control the receiver
    await receiver.main.set_volume(-30.0)
    await receiver.main.select_input_source(InputSource.DVD)

    await receiver.disconnect()

asyncio.run(main())

CLI

A built-in CLI lets you quickly test your serial connection:

# Query and print receiver status
python -m denon_rs232 /dev/ttyUSB0

# Also probe which input sources the receiver accepts
python -m denon_rs232 /dev/ttyUSB0 --probe

# Use legacy zone 3 prefix for AVR-3803/3805
python -m denon_rs232 /dev/ttyUSB0 --zone3-prefix Z1

Features

Full state after query

connect() only opens and verifies the serial connection. Call query_state() when you want the current receiver state populated into the state property. After that, state is kept up to date via events from the receiver.

Control lives on shared player objects:

receiver.main
receiver.zone_2
receiver.zone_3
receiver = DenonReceiver("/dev/ttyUSB0")
await receiver.connect()
await receiver.query_state()

state = receiver.state
state.power          # PowerState.ON / PowerState.STANDBY
state.main_zone_power      # True / False
state.volume         # float in dB (0.0 = reference, -80.0 = min, +18.0 = max)
state.mute           # True / False
state.input_source   # InputSource enum
state.surround_mode  # str (e.g. "STEREO", "DOLBY DIGITAL", "DTS SURROUND")
state.digital_input  # DigitalInputMode enum
state.video_select   # InputSource or None
state.rec_select     # InputSource or None

Event subscription

Subscribe to state changes to react in real-time. Callbacks receive a ReceiverState snapshot on updates, or None when the connection is lost.

def on_state_change(state):
    if state is None:
        print("Disconnected!")
        return
    print(f"Volume: {state.volume} dB, Source: {state.input_source}")

unsub = receiver.subscribe(on_state_change)
# Later:
unsub()  # stop receiving events

Receiver power

await receiver.power_on()
await receiver.power_standby()
power = await receiver.query_power()  # bool

Main zone

await receiver.main.power_on()
await receiver.main.power_standby()
on = await receiver.main.query_power()  # bool

Master volume

Volume is represented in dB: 0.0 dB is the reference level, -80.0 is minimum, +18.0 is maximum. Half-dB steps are supported.

await receiver.main.set_volume(-25.0)     # set to -25 dB
await receiver.main.set_volume(-25.5)     # half-dB step
await receiver.main.volume_up()
await receiver.main.volume_down()
db = await receiver.main.query_volume()   # float

Channel volumes

Individual channel levels, relative to the master volume. 0.0 dB is neutral, range is -12.0 to +12.0 dB. Available channels depend on the speaker configuration: FL, FR, C, SW, SL, SR, SBL, SBR, SB.

await receiver.main.set_channel_volume("FL", 2.0)   # front left +2 dB
await receiver.main.set_channel_volume("SW", -3.5)  # subwoofer -3.5 dB
await receiver.main.channel_volume_up("C")
await receiver.main.channel_volume_down("FR")

# All channel volumes are in state after connect:
state.channel_volumes  # {"FL": 0.0, "FR": 0.0, "C": -1.0, ...}

Mute

await receiver.main.mute_on()
await receiver.main.mute_off()
muted = await receiver.main.query_mute()  # bool

Input source

from denon_rs232 import InputSource

await receiver.main.select_input_source(InputSource.BD)
source = await receiver.main.query_input_source()  # InputSource enum

Available sources depend on the model. See Input sources below.

Surround mode

Surround mode is kept as a plain string because receivers return many combined mode names (e.g. "DOLBY D+PL2X C", "DTS HD MSTR").

await receiver.main.set_surround_mode("STEREO")
await receiver.main.set_surround_mode("DOLBY DIGITAL")
await receiver.main.set_surround_mode("DTS SURROUND")
await receiver.main.set_surround_mode("DIRECT")
await receiver.main.set_surround_mode("PURE DIRECT")
await receiver.main.set_surround_mode("MCH STEREO")
mode = await receiver.main.query_surround_mode()  # str

Digital input mode

from denon_rs232 import DigitalInputMode

await receiver.main.set_digital_input(DigitalInputMode.AUTO)
await receiver.main.set_digital_input(DigitalInputMode.HDMI)
await receiver.main.set_digital_input(DigitalInputMode.DIGITAL)
await receiver.main.set_digital_input(DigitalInputMode.ANALOG)
mode = await receiver.main.query_digital_input()  # DigitalInputMode enum or None ("NO")

Legacy models also support PCM, DTS, RF, EXT_IN_1, EXT_IN_2.

Video / recording source select

Override the video or recording source independently from the main input source:

await receiver.main.set_video_select(InputSource.DVD)
await receiver.main.cancel_video_select()  # return to following input
source = await receiver.main.query_video_select()

await receiver.main.set_rec_select(InputSource.CD)
await receiver.main.cancel_rec_select()
source = await receiver.main.query_rec_select()

Parameter settings

from denon_rs232 import SurroundBack, ModeSetting, RoomEQ

# Tone defeat
await receiver.main.tone_defeat_on()
await receiver.main.tone_defeat_off()

# Surround back speakers
await receiver.main.set_surround_back(SurroundBack.PL2X_CINEMA)
await receiver.main.set_surround_back(SurroundBack.OFF)

# Cinema EQ
await receiver.main.cinema_eq_on()
await receiver.main.cinema_eq_off()

# Decoder mode
await receiver.main.set_mode_setting(ModeSetting.CINEMA)
await receiver.main.set_mode_setting(ModeSetting.MUSIC)

# Room EQ (pre-Audyssey models)
await receiver.main.set_room_eq(RoomEQ.FLAT)

All parameter settings are available in state after connect:

state.tone_defeat     # bool
state.surround_back   # SurroundBack enum
state.cinema_eq       # bool
state.mode_setting    # ModeSetting enum
state.room_eq         # RoomEQ enum (event-only, not in PS? response)

Tuner

from denon_rs232 import TunerBand, TunerMode

await receiver.main.set_tuner_band(TunerBand.FM)
await receiver.main.set_tuner_mode(TunerMode.AUTO)
await receiver.main.set_tuner_frequency("105000")  # FM 105.0 MHz
await receiver.main.set_tuner_preset("A1")
await receiver.main.tuner_frequency_up()
await receiver.main.tuner_frequency_down()
await receiver.main.tuner_preset_up()
await receiver.main.tuner_preset_down()

freq = await receiver.main.query_tuner_frequency()  # str
preset = await receiver.main.query_tuner_preset()   # str

Tuner band and mode are available via events (state.tuner_band, state.tuner_mode).

Multi-zone

Zone 2 and Zone 3 can be controlled independently. Zone state (power, source, volume) is populated by query_state() and updated via events.

# Zone 2
await receiver.zone_2.power_on()
await receiver.zone_2.power_standby()
await receiver.zone_2.select_input_source(InputSource.TUNER)
await receiver.zone_2.set_volume(-30.0)
await receiver.zone_2.volume_up()
await receiver.zone_2.volume_down()

# Zone 3
await receiver.zone_3.power_on()
await receiver.zone_3.power_standby()
await receiver.zone_3.select_input_source(InputSource.CD)
await receiver.zone_3.set_volume(-35.0)
await receiver.zone_3.volume_up()
await receiver.zone_3.volume_down()

Zone state in state:

state.zone_2.power   # bool
state.zone_2.input_source  # InputSource
state.zone_2.volume  # float in dB
state.zone_3.power   # bool
state.zone_3.input_source  # InputSource
state.zone_3.volume  # float in dB

Zone 3 prefix: Legacy models (AVR-3803, AVR-3805) use the Z1 command prefix for Zone 3. Modern models use Z3. The default is Z3; pass zone3_prefix="Z1" for legacy models:

receiver = DenonReceiver("/dev/ttyUSB0", zone3_prefix="Z1")

Source probing

Discover which input sources the receiver actually supports by trying each one:

sources = await receiver.probe_sources()
# frozenset({InputSource.CD, InputSource.DVD, InputSource.TUNER, ...})

This briefly switches through all input sources and restores the original when done. Nothing should be playing during probing.

Receiver models

Pre-defined model capabilities are available in denon_rs232.models:

from denon_rs232.models import AVR_3805, AVR_X4000, ALL_MODELS

# Check if a source is supported by a specific model
InputSource.BD in AVR_X4000.input_sources       # True
InputSource.BD in AVR_3805.input_sources         # False

# Get the zone 3 prefix for a model
AVR_3805.zone3_prefix   # "Z1"
AVR_X4000.zone3_prefix  # "Z3"

# Iterate all models
for model in ALL_MODELS:
    print(f"{model.name}: {len(model.input_sources)} sources")

Available models:

Constant Models Era Zone 3 Digital
AVR_3803 AVR-3803 / AVC-3570 / AVR-2803 ~2003 Z1 Gen 1 (PCM/DTS/RF)
AVR_3805 AVR-3805 / AVC-3890 ~2004 Z1 Gen 1 (PCM/DTS)
AVR_987 AVR-987 ~2005 Z3 Gen 1
AVR_2308CI AVR-2308CI / AVC-2308 ~2007 -- Gen 1
AVR_2808CI AVR-2808CI / AVC-2808 / AVR-988 ~2007 Z3 Gen 1
AVR_4308CI AVR-4308CI ~2008 Z3 Gen 1
AVR_3310CI AVR-3310CI / AVR-990 / AVC-3310 ~2009 Z3 Gen 2 (HDMI/DIGITAL)
AVR_X1000 AVR-X1000 / AVR-E300 ~2013 -- Gen 3 (HDMI/DIGITAL)
AVR_X4000 AVR-X4000 ~2013 Z3 Gen 3
AVR_X4200W AVR-X4200W / X3200W / X2200W / X1200W ~2015 Z3 Gen 3

Connection handling

The library handles connection errors gracefully:

  • If the receiver doesn't respond during connect(), a ConnectionError is raised.
  • If the serial connection is lost (cable unplugged, device error), subscribers receive None and connected becomes False.
  • Write errors during commands propagate the exception and tear down the connection.
try:
    await receiver.connect()
except ConnectionError:
    print("Receiver not responding")

Input sources

Available input sources vary by model era:

Source Protocol value Era
PHONO PHONO Legacy
CD CD Legacy
TUNER TUNER Legacy
DVD DVD Legacy
VDP VDP Legacy
TV TV Legacy
DBS_SAT DBS/SAT Legacy
VCR_1 VCR-1 Legacy
VCR_2 VCR-2 Legacy
VCR_3 VCR-3 Legacy
V_AUX V.AUX Legacy
CDR_TAPE1 CDR/TAPE1 Legacy
MD_TAPE2 MD/TAPE2 Legacy
HDP HDP Transition
DVR DVR Transition
TV_CBL TV/CBL Transition
SAT SAT Transition
NET_USB NET/USB Transition
DOCK DOCK Transition
IPOD IPOD Transition
BD BD Modern
SAT_CBL SAT/CBL Modern
MPLAY MPLAY Modern
GAME GAME Modern
AUX1 AUX1 Modern
AUX2 AUX2 Modern
NET NET Modern
BT BT Modern
USB_IPOD USB/IPOD Modern
PANDORA PANDORA Streaming
SIRIUSXM SIRIUSXM Streaming
SPOTIFY SPOTIFY Streaming
FLICKR FLICKR Streaming
IRADIO IRADIO Streaming
SERVER SERVER Streaming
FAVORITES FAVORITES Streaming
LASTFM LASTFM Streaming
XM XM Radio
SIRIUS SIRIUS Radio
HDRADIO HDRADIO Radio
DAB DAB Radio

Not all sources exist on every receiver. Use probe_sources() or a ReceiverModel definition to determine which sources your receiver supports.

Serial connection

The library uses serialx for async serial communication. All Denon RS232 receivers use 9600 baud, 8 data bits, no parity, 1 stop bit.

Most receivers have a DB-9 connector. The AVR-3803 / AVC-3570 uses a 3.5mm stereo mini plug (Tip=RXD, Ring=TXD, Sleeve=GND).

Development

# Install dev dependencies
uv sync

# Run tests
uv run pytest

# Run tests with verbose output
uv run pytest -v

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

denon_rs232-3.0.0.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

denon_rs232-3.0.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file denon_rs232-3.0.0.tar.gz.

File metadata

  • Download URL: denon_rs232-3.0.0.tar.gz
  • Upload date:
  • Size: 15.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for denon_rs232-3.0.0.tar.gz
Algorithm Hash digest
SHA256 67778ec221621b0c8924c804fbf1766f673b12ea6e528e08e108eccb249a3877
MD5 e614a8956aaa8956b156feef2016e7b3
BLAKE2b-256 ba6196a8123746dbb84d0927f0c08492e6cc836d3f5131beeafd120038fba2b2

See more details on using hashes here.

Provenance

The following attestation bundles were made for denon_rs232-3.0.0.tar.gz:

Publisher: publish.yml on home-assistant-libs/denon-rs232

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file denon_rs232-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: denon_rs232-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for denon_rs232-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 73d75b1825e0559ce0caa0cc70b6961ce08ca60a71c1509691d8577e720a1770
MD5 ac170932a414953dcd17295a1c6a2104
BLAKE2b-256 973778533386ef643f5d7a0732040f85ce1d60a5dce7063478832daa132937c4

See more details on using hashes here.

Provenance

The following attestation bundles were made for denon_rs232-3.0.0-py3-none-any.whl:

Publisher: publish.yml on home-assistant-libs/denon-rs232

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page