Async library to control Marantz receivers over RS232
Project description
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\rframing): NR1506, NR1606, SR5010, SR6010, SR7010, AV7702mkII —MarantzReceiver. - Legacy (2007–2010 lineup,
@CMD:VALUE\rframing): 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_input_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_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, 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_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 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_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()
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_standby()
await receiver.zone_3.select_input_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(), aConnectionErroris raised. - If the serial connection is lost (cable unplugged, device error), subscribers receive
NoneandconnectedbecomesFalse. - 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file marantz_rs232-1.0.0.tar.gz.
File metadata
- Download URL: marantz_rs232-1.0.0.tar.gz
- Upload date:
- Size: 53.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27e1dc7c890f32ed3943c3e84f7ba9ef6b535205f20cc60f1d6ed5f6d5fbc540
|
|
| MD5 |
1b91bbea1c2034a4fb6083435eb76142
|
|
| BLAKE2b-256 |
f12b29ae6b25804a7f48233edab9434d73033770641a2fa920640d8bbb0c98eb
|
Provenance
The following attestation bundles were made for marantz_rs232-1.0.0.tar.gz:
Publisher:
publish.yml on home-assistant-libs/marantz-rs232
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
marantz_rs232-1.0.0.tar.gz -
Subject digest:
27e1dc7c890f32ed3943c3e84f7ba9ef6b535205f20cc60f1d6ed5f6d5fbc540 - Sigstore transparency entry: 1480971324
- Sigstore integration time:
-
Permalink:
home-assistant-libs/marantz-rs232@30e06e876a96c9d7cde6b054f350a48af0b82220 -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/home-assistant-libs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@30e06e876a96c9d7cde6b054f350a48af0b82220 -
Trigger Event:
release
-
Statement type:
File details
Details for the file marantz_rs232-1.0.0-py3-none-any.whl.
File metadata
- Download URL: marantz_rs232-1.0.0-py3-none-any.whl
- Upload date:
- Size: 63.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
964c1e06c088ff7cfa9b891bdf8181e981deabe0bcb2b157eaf873dded2e6773
|
|
| MD5 |
e08e945239b16b4f6d2a80e279ed40bf
|
|
| BLAKE2b-256 |
0fdf28f8ee42c89aa3be799340781a2cec8e886df9afe55dd126f8e9ff8ba0f4
|
Provenance
The following attestation bundles were made for marantz_rs232-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on home-assistant-libs/marantz-rs232
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
marantz_rs232-1.0.0-py3-none-any.whl -
Subject digest:
964c1e06c088ff7cfa9b891bdf8181e981deabe0bcb2b157eaf873dded2e6773 - Sigstore transparency entry: 1480971367
- Sigstore integration time:
-
Permalink:
home-assistant-libs/marantz-rs232@30e06e876a96c9d7cde6b054f350a48af0b82220 -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/home-assistant-libs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@30e06e876a96c9d7cde6b054f350a48af0b82220 -
Trigger Event:
release
-
Statement type: