Skip to main content

marantz-rs232

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

Supports two distinct Marantz protocols:

  • Modern (2015 lineup, PREFIX+VALUE\r framing): NR1506, NR1606, SR5010, SR6010, SR7010, AV7702mkII — MarantzReceiver.
  • Legacy (2007–2010 lineup, @CMD:VALUE\r framing): SR7002, SR8002, SR6003, SR7003, SR8003, SR5004, SR6004, AV7005, AV8003 — MarantzLegacyReceiver.

If you don't know which protocol your receiver speaks, use probe() to auto-detect.

Installation

pip install marantz-rs232

Requires Python 3.12+.

Quick start

Modern receivers (2015 lineup)

import asyncio
from marantz_rs232 import MarantzReceiver, InputSource

async def main():
    receiver = MarantzReceiver("/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.main_zone.volume} dB")
    print(f"Input: {receiver.state.main_zone.input_source}")

    # Control the receiver
    await receiver.main.set_volume(-30.0)
    await receiver.main.select_source(InputSource.BD)

    await receiver.disconnect()

asyncio.run(main())

Legacy receivers (SR7002 era)

import asyncio
from marantz_rs232 import MarantzLegacyReceiver, LegacySource, LegacyModel

async def main():
    # `model` is optional — defaults to GENERIC. Pass SR8002 to enable
    # Multi Room B and HD Radio metadata.
    receiver = MarantzLegacyReceiver("/dev/ttyUSB0", model=LegacyModel.SR7002)
    await receiver.connect()
    await receiver.query_state()

    print(f"Power: {receiver.state.main.power}")
    print(f"Volume: {receiver.state.main.volume} dB")
    print(f"Surround: {receiver.state.main.surround_mode}")

    await receiver.main.set_volume(-30.0)
    await receiver.main.select_source(LegacySource.CD_CDR)

    await receiver.disconnect()

asyncio.run(main())

Auto-detect (don't know which protocol)

from marantz_rs232 import probe

cls = await probe("/dev/ttyUSB0")  # MarantzReceiver or MarantzLegacyReceiver
receiver = cls("/dev/ttyUSB0")
await receiver.connect()

CLI

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

# Modern receiver (default)
python -m marantz_rs232 /dev/ttyUSB0

# Modern + probe input sources
python -m marantz_rs232 /dev/ttyUSB0 --probe

# Legacy (SR7002-era) receiver
python -m marantz_rs232 /dev/ttyUSB0 --legacy
python -m marantz_rs232 /dev/ttyUSB0 --legacy --model SR8002

# Auto-detect protocol on the wire
python -m marantz_rs232 /dev/ttyUSB0 --detect

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 = MarantzReceiver("/dev/ttyUSB0")
await receiver.connect()
await receiver.query_state()

state = receiver.state
state.power                     # True / False (overall power)
state.main_zone.power           # True / False (main zone)
state.main_zone.volume          # float in dB (0.0 = reference, -80.0 = min, +18.0 = max)
state.main_zone.mute            # True / False
state.main_zone.input_source    # InputSource enum
state.main_zone.surround_mode   # str (e.g. "STEREO", "DOLBY DIGITAL", "DTS SURROUND")
state.main_zone.digital_input   # DigitalInputMode enum
state.main_zone.audio_decode    # AudioDecodeMode enum (AUTO / PCM / DTS)
state.main_zone.video_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
    mz = state.main_zone
    print(f"Volume: {mz.volume} dB, Source: {mz.input_source}")

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

Receiver power

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

Main zone

await receiver.main.power_on()
await receiver.main.power_off()
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, FH, FW.

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.main_zone.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 marantz_rs232 import InputSource

await receiver.main.select_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 DIGITAL", "DTS SURROUND", "AURO3D").

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")
await receiver.main.set_surround_mode("AURO3D")
mode = await receiver.main.query_surround_mode()  # str

Digital input mode

from marantz_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)
await receiver.main.set_digital_input(DigitalInputMode.EXT_IN)
await receiver.main.set_digital_input(DigitalInputMode.SEVEN_1_IN)
mode = await receiver.main.query_digital_input()  # DigitalInputMode enum or None ("NO")

Audio decode

from marantz_rs232 import AudioDecodeMode

await receiver.main.set_audio_decode(AudioDecodeMode.AUTO)
await receiver.main.set_audio_decode(AudioDecodeMode.PCM)
await receiver.main.set_audio_decode(AudioDecodeMode.DTS)
mode = await receiver.main.query_audio_decode()

Video select

Override the video 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()

Tone control

# Tone control on/off
await receiver.main.tone_control_on()
await receiver.main.tone_control_off()

# Bass / treble: dB values from -6 to +6
await receiver.main.set_bass(3)
await receiver.main.set_treble(-2)
await receiver.main.bass_up()
await receiver.main.bass_down()
await receiver.main.treble_up()
await receiver.main.treble_down()

Audyssey / EQ settings

from marantz_rs232 import MultEQ, DynamicVolume, DRC

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

# MultEQ XT/XT32
await receiver.main.set_multeq(MultEQ.AUDYSSEY)
await receiver.main.set_multeq(MultEQ.FLAT)
await receiver.main.set_multeq(MultEQ.OFF)

# Dynamic EQ
await receiver.main.dynamic_eq_on()
await receiver.main.dynamic_eq_off()

# Dynamic Volume
await receiver.main.set_dynamic_volume(DynamicVolume.MED)
await receiver.main.set_dynamic_volume(DynamicVolume.OFF)

# Dynamic Range Compression
await receiver.main.set_drc(DRC.AUTO)
await receiver.main.set_drc(DRC.HI)

All parameter settings are available in state after connect:

state.main_zone.tone_control     # bool
state.main_zone.bass             # float
state.main_zone.treble           # float
state.main_zone.cinema_eq        # bool
state.main_zone.multeq           # MultEQ enum
state.main_zone.dynamic_eq       # bool
state.main_zone.dynamic_volume   # DynamicVolume enum
state.main_zone.drc              # DRC enum

Sleep / ECO / Standby / Dimmer

from marantz_rs232 import EcoMode, DimmerMode

# Sleep timer (minutes)
await receiver.main.set_sleep(30)
await receiver.main.sleep_off()

# ECO mode
await receiver.main.set_eco(EcoMode.AUTO)
await receiver.main.set_eco(EcoMode.ON)
await receiver.main.set_eco(EcoMode.OFF)

# Auto standby
await receiver.main.set_auto_standby("2H")
await receiver.main.auto_standby_off()

# Front-panel dimmer
await receiver.main.set_dimmer(DimmerMode.BRI)
await receiver.main.set_dimmer(DimmerMode.DIM)
await receiver.main.set_dimmer(DimmerMode.DAR)
await receiver.main.set_dimmer(DimmerMode.OFF)

Tuner

from marantz_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 in state (state.main_zone.tuner_band, state.main_zone.tuner_mode).

Multi-zone

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

# Zone 2
await receiver.zone_2.power_on()
await receiver.zone_2.power_off()
await receiver.zone_2.select_source(InputSource.TUNER)
await receiver.zone_2.set_volume(-30.0)
await receiver.zone_2.volume_up()
await receiver.zone_2.volume_down()
await receiver.zone_2.mute_on()
await receiver.zone_2.mute_off()

# Zone 3
await receiver.zone_3.power_on()
await receiver.zone_3.power_off()
await receiver.zone_3.select_source(InputSource.CD)
await receiver.zone_3.set_volume(-35.0)
await receiver.zone_3.mute_on()
await receiver.zone_3.mute_off()

Zone state in state:

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

Source probing

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

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

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

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

Source Protocol value
PHONO PHONO
CD CD
TUNER TUNER
DVD DVD
BD BD
TV TV
SAT_CBL SAT/CBL
SAT SAT
MPLAY MPLAY
VCR VCR
GAME GAME
V_AUX V.AUX
HDRADIO HDRADIO
SIRIUS SIRIUS
SIRIUSXM SIRIUSXM
SPOTIFY SPOTIFY
RHAPSODY RHAPSODY
PANDORA PANDORA
NAPSTER NAPSTER
LASTFM LASTFM
FLICKR FLICKR
IRADIO IRADIO
SERVER SERVER
FAVORITES FAVORITES
CDR CDR
AUX1 - AUX7 AUX1-AUX7
NET NET
NET_USB NET/USB
BT BT
M_XPORT MXPORT
USB_IPOD USB/IPOD

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

Serial connection

The library uses serialx for async serial communication. Marantz RS232 receivers use 9600 baud, 8 data bits, no parity, 1 stop bit (8N1) on a DB-9 connector.

Legacy receivers (SR7002 era)

For 2007–2010 Marantz units that speak the older @CMD:VALUE\r protocol, use MarantzLegacyReceiver. The full SR7002/SR8002 spec is implemented: power, mute (audio + video), attenuator, 7.1 ch input, volume (with .5 dB encoding), tone, source (2-character video+audio status), speaker A/B, HDMI out + audio mode, IP converter, surround mode, THX, EQ mode, Dolby Headphone, night mode, M-DAX, lip sync, sleep, menu, cursor, front-key lock, DC triggers, test tone, full tuner family (AM/FM/XM frequency, presets, mode, memory/clear), XM navigation and metadata, status-only signal info (input AD, signal type/state, signal format, sampling frequency, channel status, firmware version, auto lip sync), full Multi Room A, plus SR8002-only Multi Room B (= separator) and HD Radio metadata (* separator).

from marantz_rs232 import MarantzLegacyReceiver, LegacyModel, LegacySource, LegacyTHXSet

# Pass model=LegacyModel.SR8002 to unlock SR8002-only features without warnings.
receiver = MarantzLegacyReceiver("/dev/ttyUSB0", model=LegacyModel.SR7002)
await receiver.connect()

# Main zone control mirrors the modern API where possible.
await receiver.main.power_on()
await receiver.main.set_volume(-25.0)
await receiver.main.set_thx_mode(LegacyTHXSet.CINEMA)
await receiver.main.set_tuner_fm_frequency(101.10)

# Multi Room A (also Multi Room B on SR8002).
await receiver.multi_room_a.power_on()
await receiver.multi_room_a.set_line_volume(-30.0)

# Auto-status feedback (`@AST:F`) is enabled on connect, so subscribers
# see spontaneous receiver state changes the same way as the modern API.
unsub = receiver.subscribe(lambda state: print(f"changed: {state.main.volume} dB"))

receiver.state is a LegacyReceiverState. The schema differs from the modern receiver — see marantz_rs232.legacy.LegacyMainState for the field list.

Auto-detect

from marantz_rs232 import probe

# Probes both protocols and returns whichever class matches the wire.
cls = await probe("/dev/ttyUSB0")
receiver = cls("/dev/ttyUSB0")
await receiver.connect()

Supported models

Class Protocol Models
MarantzReceiver 2015 IP/RS-232 (PREFIX+VALUE\r) NR1506, NR1606, SR5010, SR6010, SR7010, AV7702mkII
MarantzLegacyReceiver 2007 RS-232 (@CMD:VALUE\r) SR7002, SR8002, SR6003, SR7003, SR8003, SR5004, SR6004, AV7005, AV8003

The 2015 protocol is documented in docs/Marantz 2015 NR_SR_AV IP-232 Protocol.xls. The legacy protocol is documented in docs/Marantz 2007 SR7002 SR8002 RS232C Control Specification v1.00.pdf. Other Marantz receivers from the same era using the same command set should also work, possibly with a few unsupported commands.

Development

# Install dev dependencies
uv sync

# Run tests
uv run pytest

# Run tests with verbose output
uv run pytest -v

License

MIT

Release files for marantz-rs232 2.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for marantz-rs232 2.0.1
File Size Uploaded
marantz_rs232-2.0.1.tar.gz 53.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for marantz-rs232 2.0.1
File Interpreter ABI Platform
marantz_rs232-2.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 117.1 kB

Release files / marantz_rs232-2.0.1.tar.gz

Download URL marantz_rs232-2.0.1.tar.gz
Size 53.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2a71c19b4956a40202cd82c4af4b57e826be0eae609343193871f829a8cd8bb2
BLAKE2b-256 checksum
How to use checksums
99d73995abdcb6f9fc92bf2350f36956ea41bd9363d5a6bb2db76f4369234269
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 19, 2026.

Transparency log

Release files / marantz_rs232-2.0.1-py3-none-any.whl

Download URL marantz_rs232-2.0.1-py3-none-any.whl
Size 63.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8520f4baf580ea8b7f921300b4284e18b4107669ef509e0c11496981481e3a62
BLAKE2b-256 checksum
How to use checksums
b94a031bbc8b8c6389c9ae6fe3522509d42dbdaa9018f7ed128ff69003333a85
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.0.1 This release

2 release files

2.0.0

2 release files

1.0.0

2 release files

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