marantz-rs232 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,551 @@
1
+ Metadata-Version: 2.4
2
+ Name: marantz-rs232
3
+ Version: 1.0.0
4
+ Summary: Async library to control Marantz receivers over RS232
5
+ Author: Paulus Schoutsen
6
+ Author-email: Paulus Schoutsen <balloob@gmail.com>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Home Automation
16
+ Classifier: Topic :: System :: Hardware
17
+ Classifier: Framework :: AsyncIO
18
+ Requires-Dist: serialx[esphome]>=1.2.0
19
+ Requires-Python: >=3.12
20
+ Project-URL: Repository, https://github.com/home-assistant-libs/marantz-rs232
21
+ Description-Content-Type: text/markdown
22
+
23
+ # marantz-rs232
24
+
25
+ Async Python library to control Marantz AV receivers over RS232 serial, built on [serialx](https://github.com/puddly/serialx).
26
+
27
+ Supports two distinct Marantz protocols:
28
+
29
+ - **Modern** (2015 lineup, `PREFIX+VALUE\r` framing): NR1506, NR1606, SR5010, SR6010, SR7010, AV7702mkII — `MarantzReceiver`.
30
+ - **Legacy** (2007–2010 lineup, `@CMD:VALUE\r` framing): SR7002, SR8002, SR6003, SR7003, SR8003, SR5004, SR6004, AV7005, AV8003 — `MarantzLegacyReceiver`.
31
+
32
+ If you don't know which protocol your receiver speaks, use `probe()` to auto-detect.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install marantz-rs232
38
+ ```
39
+
40
+ Requires Python 3.12+.
41
+
42
+ ## Quick start
43
+
44
+ ### Modern receivers (2015 lineup)
45
+
46
+ ```python
47
+ import asyncio
48
+ from marantz_rs232 import MarantzReceiver, InputSource
49
+
50
+ async def main():
51
+ receiver = MarantzReceiver("/dev/ttyUSB0")
52
+ await receiver.connect()
53
+ await receiver.query_state()
54
+
55
+ # State is fully populated after query_state()
56
+ print(f"Power: {receiver.state.power}")
57
+ print(f"Volume: {receiver.state.main_zone.volume} dB")
58
+ print(f"Input: {receiver.state.main_zone.input_source}")
59
+
60
+ # Control the receiver
61
+ await receiver.main.set_volume(-30.0)
62
+ await receiver.main.select_input_source(InputSource.BD)
63
+
64
+ await receiver.disconnect()
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ### Legacy receivers (SR7002 era)
70
+
71
+ ```python
72
+ import asyncio
73
+ from marantz_rs232 import MarantzLegacyReceiver, LegacySource, LegacyModel
74
+
75
+ async def main():
76
+ # `model` is optional — defaults to GENERIC. Pass SR8002 to enable
77
+ # Multi Room B and HD Radio metadata.
78
+ receiver = MarantzLegacyReceiver("/dev/ttyUSB0", model=LegacyModel.SR7002)
79
+ await receiver.connect()
80
+ await receiver.query_state()
81
+
82
+ print(f"Power: {receiver.state.main.power}")
83
+ print(f"Volume: {receiver.state.main.volume} dB")
84
+ print(f"Surround: {receiver.state.main.surround_mode}")
85
+
86
+ await receiver.main.set_volume(-30.0)
87
+ await receiver.main.select_source(LegacySource.CD_CDR)
88
+
89
+ await receiver.disconnect()
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ### Auto-detect (don't know which protocol)
95
+
96
+ ```python
97
+ from marantz_rs232 import probe
98
+
99
+ cls = await probe("/dev/ttyUSB0") # MarantzReceiver or MarantzLegacyReceiver
100
+ receiver = cls("/dev/ttyUSB0")
101
+ await receiver.connect()
102
+ ```
103
+
104
+ ## CLI
105
+
106
+ A built-in CLI lets you quickly test your serial connection:
107
+
108
+ ```bash
109
+ # Modern receiver (default)
110
+ python -m marantz_rs232 /dev/ttyUSB0
111
+
112
+ # Modern + probe input sources
113
+ python -m marantz_rs232 /dev/ttyUSB0 --probe
114
+
115
+ # Legacy (SR7002-era) receiver
116
+ python -m marantz_rs232 /dev/ttyUSB0 --legacy
117
+ python -m marantz_rs232 /dev/ttyUSB0 --legacy --model SR8002
118
+
119
+ # Auto-detect protocol on the wire
120
+ python -m marantz_rs232 /dev/ttyUSB0 --detect
121
+ ```
122
+
123
+ ## Features
124
+
125
+ ### Full state after query
126
+
127
+ `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.
128
+
129
+ Control lives on shared player objects:
130
+
131
+ ```python
132
+ receiver.main
133
+ receiver.zone_2
134
+ receiver.zone_3
135
+ ```
136
+
137
+ ```python
138
+ receiver = MarantzReceiver("/dev/ttyUSB0")
139
+ await receiver.connect()
140
+ await receiver.query_state()
141
+
142
+ state = receiver.state
143
+ state.power # True / False (overall power)
144
+ state.main_zone.power # True / False (main zone)
145
+ state.main_zone.volume # float in dB (0.0 = reference, -80.0 = min, +18.0 = max)
146
+ state.main_zone.mute # True / False
147
+ state.main_zone.input_source # InputSource enum
148
+ state.main_zone.surround_mode # str (e.g. "STEREO", "DOLBY DIGITAL", "DTS SURROUND")
149
+ state.main_zone.digital_input # DigitalInputMode enum
150
+ state.main_zone.audio_decode # AudioDecodeMode enum (AUTO / PCM / DTS)
151
+ state.main_zone.video_select # InputSource or None
152
+ ```
153
+
154
+ ### Event subscription
155
+
156
+ Subscribe to state changes to react in real-time. Callbacks receive a `ReceiverState` snapshot on updates, or `None` when the connection is lost.
157
+
158
+ ```python
159
+ def on_state_change(state):
160
+ if state is None:
161
+ print("Disconnected!")
162
+ return
163
+ mz = state.main_zone
164
+ print(f"Volume: {mz.volume} dB, Source: {mz.input_source}")
165
+
166
+ unsub = receiver.subscribe(on_state_change)
167
+ # Later:
168
+ unsub() # stop receiving events
169
+ ```
170
+
171
+ ### Receiver power
172
+
173
+ ```python
174
+ await receiver.power_on()
175
+ await receiver.power_standby()
176
+ power = await receiver.query_power() # bool
177
+ ```
178
+
179
+ ### Main zone
180
+
181
+ ```python
182
+ await receiver.main.power_on()
183
+ await receiver.main.power_standby()
184
+ on = await receiver.main.query_power() # bool
185
+ ```
186
+
187
+ ### Master volume
188
+
189
+ 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.
190
+
191
+ ```python
192
+ await receiver.main.set_volume(-25.0) # set to -25 dB
193
+ await receiver.main.set_volume(-25.5) # half-dB step
194
+ await receiver.main.volume_up()
195
+ await receiver.main.volume_down()
196
+ db = await receiver.main.query_volume() # float
197
+ ```
198
+
199
+ ### Channel volumes
200
+
201
+ 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.
202
+
203
+ ```python
204
+ await receiver.main.set_channel_volume("FL", 2.0) # front left +2 dB
205
+ await receiver.main.set_channel_volume("SW", -3.5) # subwoofer -3.5 dB
206
+ await receiver.main.channel_volume_up("C")
207
+ await receiver.main.channel_volume_down("FR")
208
+
209
+ # All channel volumes are in state after connect:
210
+ state.main_zone.channel_volumes # {"FL": 0.0, "FR": 0.0, "C": -1.0, ...}
211
+ ```
212
+
213
+ ### Mute
214
+
215
+ ```python
216
+ await receiver.main.mute_on()
217
+ await receiver.main.mute_off()
218
+ muted = await receiver.main.query_mute() # bool
219
+ ```
220
+
221
+ ### Input source
222
+
223
+ ```python
224
+ from marantz_rs232 import InputSource
225
+
226
+ await receiver.main.select_input_source(InputSource.BD)
227
+ source = await receiver.main.query_input_source() # InputSource enum
228
+ ```
229
+
230
+ Available sources depend on the model. See [Input sources](#input-sources) below.
231
+
232
+ ### Surround mode
233
+
234
+ Surround mode is kept as a plain string because receivers return many combined mode names (e.g. `"DOLBY DIGITAL"`, `"DTS SURROUND"`, `"AURO3D"`).
235
+
236
+ ```python
237
+ await receiver.main.set_surround_mode("STEREO")
238
+ await receiver.main.set_surround_mode("DOLBY DIGITAL")
239
+ await receiver.main.set_surround_mode("DTS SURROUND")
240
+ await receiver.main.set_surround_mode("DIRECT")
241
+ await receiver.main.set_surround_mode("PURE DIRECT")
242
+ await receiver.main.set_surround_mode("MCH STEREO")
243
+ await receiver.main.set_surround_mode("AURO3D")
244
+ mode = await receiver.main.query_surround_mode() # str
245
+ ```
246
+
247
+ ### Digital input mode
248
+
249
+ ```python
250
+ from marantz_rs232 import DigitalInputMode
251
+
252
+ await receiver.main.set_digital_input(DigitalInputMode.AUTO)
253
+ await receiver.main.set_digital_input(DigitalInputMode.HDMI)
254
+ await receiver.main.set_digital_input(DigitalInputMode.DIGITAL)
255
+ await receiver.main.set_digital_input(DigitalInputMode.ANALOG)
256
+ await receiver.main.set_digital_input(DigitalInputMode.EXT_IN)
257
+ await receiver.main.set_digital_input(DigitalInputMode.SEVEN_1_IN)
258
+ mode = await receiver.main.query_digital_input() # DigitalInputMode enum or None ("NO")
259
+ ```
260
+
261
+ ### Audio decode
262
+
263
+ ```python
264
+ from marantz_rs232 import AudioDecodeMode
265
+
266
+ await receiver.main.set_audio_decode(AudioDecodeMode.AUTO)
267
+ await receiver.main.set_audio_decode(AudioDecodeMode.PCM)
268
+ await receiver.main.set_audio_decode(AudioDecodeMode.DTS)
269
+ mode = await receiver.main.query_audio_decode()
270
+ ```
271
+
272
+ ### Video select
273
+
274
+ Override the video source independently from the main input source:
275
+
276
+ ```python
277
+ await receiver.main.set_video_select(InputSource.DVD)
278
+ await receiver.main.cancel_video_select() # return to following input
279
+ source = await receiver.main.query_video_select()
280
+ ```
281
+
282
+ ### Tone control
283
+
284
+ ```python
285
+ # Tone control on/off
286
+ await receiver.main.tone_control_on()
287
+ await receiver.main.tone_control_off()
288
+
289
+ # Bass / treble: dB values from -6 to +6
290
+ await receiver.main.set_bass(3)
291
+ await receiver.main.set_treble(-2)
292
+ await receiver.main.bass_up()
293
+ await receiver.main.bass_down()
294
+ await receiver.main.treble_up()
295
+ await receiver.main.treble_down()
296
+ ```
297
+
298
+ ### Audyssey / EQ settings
299
+
300
+ ```python
301
+ from marantz_rs232 import MultEQ, DynamicVolume, DRC
302
+
303
+ # Cinema EQ
304
+ await receiver.main.cinema_eq_on()
305
+ await receiver.main.cinema_eq_off()
306
+
307
+ # MultEQ XT/XT32
308
+ await receiver.main.set_multeq(MultEQ.AUDYSSEY)
309
+ await receiver.main.set_multeq(MultEQ.FLAT)
310
+ await receiver.main.set_multeq(MultEQ.OFF)
311
+
312
+ # Dynamic EQ
313
+ await receiver.main.dynamic_eq_on()
314
+ await receiver.main.dynamic_eq_off()
315
+
316
+ # Dynamic Volume
317
+ await receiver.main.set_dynamic_volume(DynamicVolume.MED)
318
+ await receiver.main.set_dynamic_volume(DynamicVolume.OFF)
319
+
320
+ # Dynamic Range Compression
321
+ await receiver.main.set_drc(DRC.AUTO)
322
+ await receiver.main.set_drc(DRC.HI)
323
+ ```
324
+
325
+ All parameter settings are available in `state` after connect:
326
+
327
+ ```python
328
+ state.main_zone.tone_control # bool
329
+ state.main_zone.bass # float
330
+ state.main_zone.treble # float
331
+ state.main_zone.cinema_eq # bool
332
+ state.main_zone.multeq # MultEQ enum
333
+ state.main_zone.dynamic_eq # bool
334
+ state.main_zone.dynamic_volume # DynamicVolume enum
335
+ state.main_zone.drc # DRC enum
336
+ ```
337
+
338
+ ### Sleep / ECO / Standby / Dimmer
339
+
340
+ ```python
341
+ from marantz_rs232 import EcoMode, DimmerMode
342
+
343
+ # Sleep timer (minutes)
344
+ await receiver.main.set_sleep(30)
345
+ await receiver.main.sleep_off()
346
+
347
+ # ECO mode
348
+ await receiver.main.set_eco(EcoMode.AUTO)
349
+ await receiver.main.set_eco(EcoMode.ON)
350
+ await receiver.main.set_eco(EcoMode.OFF)
351
+
352
+ # Auto standby
353
+ await receiver.main.set_auto_standby("2H")
354
+ await receiver.main.auto_standby_off()
355
+
356
+ # Front-panel dimmer
357
+ await receiver.main.set_dimmer(DimmerMode.BRI)
358
+ await receiver.main.set_dimmer(DimmerMode.DIM)
359
+ await receiver.main.set_dimmer(DimmerMode.DAR)
360
+ await receiver.main.set_dimmer(DimmerMode.OFF)
361
+ ```
362
+
363
+ ### Tuner
364
+
365
+ ```python
366
+ from marantz_rs232 import TunerBand, TunerMode
367
+
368
+ await receiver.main.set_tuner_band(TunerBand.FM)
369
+ await receiver.main.set_tuner_mode(TunerMode.AUTO)
370
+ await receiver.main.set_tuner_frequency("105000") # FM 105.0 MHz
371
+ await receiver.main.set_tuner_preset("A1")
372
+ await receiver.main.tuner_frequency_up()
373
+ await receiver.main.tuner_frequency_down()
374
+ await receiver.main.tuner_preset_up()
375
+ await receiver.main.tuner_preset_down()
376
+
377
+ freq = await receiver.main.query_tuner_frequency() # str
378
+ preset = await receiver.main.query_tuner_preset() # str
379
+ ```
380
+
381
+ Tuner band and mode are available in state (`state.main_zone.tuner_band`, `state.main_zone.tuner_mode`).
382
+
383
+ ### Multi-zone
384
+
385
+ Zone 2 and Zone 3 can be controlled independently. Zone state (power, source, volume, mute) is populated by `query_state()` and updated via events.
386
+
387
+ ```python
388
+ # Zone 2
389
+ await receiver.zone_2.power_on()
390
+ await receiver.zone_2.power_standby()
391
+ await receiver.zone_2.select_input_source(InputSource.TUNER)
392
+ await receiver.zone_2.set_volume(-30.0)
393
+ await receiver.zone_2.volume_up()
394
+ await receiver.zone_2.volume_down()
395
+ await receiver.zone_2.mute_on()
396
+ await receiver.zone_2.mute_off()
397
+
398
+ # Zone 3
399
+ await receiver.zone_3.power_on()
400
+ await receiver.zone_3.power_standby()
401
+ await receiver.zone_3.select_input_source(InputSource.CD)
402
+ await receiver.zone_3.set_volume(-35.0)
403
+ await receiver.zone_3.mute_on()
404
+ await receiver.zone_3.mute_off()
405
+ ```
406
+
407
+ Zone state in `state`:
408
+
409
+ ```python
410
+ state.zone_2.power # bool
411
+ state.zone_2.input_source # InputSource
412
+ state.zone_2.volume # float in dB
413
+ state.zone_2.mute # bool
414
+ state.zone_3.power # bool
415
+ state.zone_3.input_source # InputSource
416
+ state.zone_3.volume # float in dB
417
+ state.zone_3.mute # bool
418
+ ```
419
+
420
+ ### Source probing
421
+
422
+ Discover which input sources the receiver actually supports by trying each one:
423
+
424
+ ```python
425
+ sources = await receiver.probe_sources()
426
+ # frozenset({InputSource.CD, InputSource.BD, InputSource.TUNER, ...})
427
+ ```
428
+
429
+ This briefly switches through all input sources and restores the original when done. Nothing should be playing during probing.
430
+
431
+ ### Connection handling
432
+
433
+ The library handles connection errors gracefully:
434
+
435
+ - If the receiver doesn't respond during `connect()`, a `ConnectionError` is raised.
436
+ - If the serial connection is lost (cable unplugged, device error), subscribers receive `None` and `connected` becomes `False`.
437
+ - Write errors during commands propagate the exception and tear down the connection.
438
+
439
+ ```python
440
+ try:
441
+ await receiver.connect()
442
+ except ConnectionError:
443
+ print("Receiver not responding")
444
+ ```
445
+
446
+ ## Input sources
447
+
448
+ | Source | Protocol value |
449
+ |--------|---------------|
450
+ | `PHONO` | PHONO |
451
+ | `CD` | CD |
452
+ | `TUNER` | TUNER |
453
+ | `DVD` | DVD |
454
+ | `BD` | BD |
455
+ | `TV` | TV |
456
+ | `SAT_CBL` | SAT/CBL |
457
+ | `SAT` | SAT |
458
+ | `MPLAY` | MPLAY |
459
+ | `VCR` | VCR |
460
+ | `GAME` | GAME |
461
+ | `V_AUX` | V.AUX |
462
+ | `HDRADIO` | HDRADIO |
463
+ | `SIRIUS` | SIRIUS |
464
+ | `SIRIUSXM` | SIRIUSXM |
465
+ | `SPOTIFY` | SPOTIFY |
466
+ | `RHAPSODY` | RHAPSODY |
467
+ | `PANDORA` | PANDORA |
468
+ | `NAPSTER` | NAPSTER |
469
+ | `LASTFM` | LASTFM |
470
+ | `FLICKR` | FLICKR |
471
+ | `IRADIO` | IRADIO |
472
+ | `SERVER` | SERVER |
473
+ | `FAVORITES` | FAVORITES |
474
+ | `CDR` | CDR |
475
+ | `AUX1` - `AUX7` | AUX1-AUX7 |
476
+ | `NET` | NET |
477
+ | `NET_USB` | NET/USB |
478
+ | `BT` | BT |
479
+ | `M_XPORT` | MXPORT |
480
+ | `USB_IPOD` | USB/IPOD |
481
+
482
+ Not all sources exist on every receiver. Use `probe_sources()` to determine which sources your receiver supports.
483
+
484
+ ## Serial connection
485
+
486
+ The library uses [serialx](https://github.com/puddly/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.
487
+
488
+ ## Legacy receivers (SR7002 era)
489
+
490
+ 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).
491
+
492
+ ```python
493
+ from marantz_rs232 import MarantzLegacyReceiver, LegacyModel, LegacySource, LegacyTHXSet
494
+
495
+ # Pass model=LegacyModel.SR8002 to unlock SR8002-only features without warnings.
496
+ receiver = MarantzLegacyReceiver("/dev/ttyUSB0", model=LegacyModel.SR7002)
497
+ await receiver.connect()
498
+
499
+ # Main zone control mirrors the modern API where possible.
500
+ await receiver.main.power_on()
501
+ await receiver.main.set_volume(-25.0)
502
+ await receiver.main.set_thx_mode(LegacyTHXSet.CINEMA)
503
+ await receiver.main.set_tuner_fm_frequency(101.10)
504
+
505
+ # Multi Room A (also Multi Room B on SR8002).
506
+ await receiver.multi_room_a.power_on()
507
+ await receiver.multi_room_a.set_line_volume(-30.0)
508
+
509
+ # Auto-status feedback (`@AST:F`) is enabled on connect, so subscribers
510
+ # see spontaneous receiver state changes the same way as the modern API.
511
+ unsub = receiver.subscribe(lambda state: print(f"changed: {state.main.volume} dB"))
512
+ ```
513
+
514
+ `receiver.state` is a `LegacyReceiverState`. The schema differs from the modern receiver — see `marantz_rs232.legacy.LegacyMainState` for the field list.
515
+
516
+ ### Auto-detect
517
+
518
+ ```python
519
+ from marantz_rs232 import probe
520
+
521
+ # Probes both protocols and returns whichever class matches the wire.
522
+ cls = await probe("/dev/ttyUSB0")
523
+ receiver = cls("/dev/ttyUSB0")
524
+ await receiver.connect()
525
+ ```
526
+
527
+ ## Supported models
528
+
529
+ | Class | Protocol | Models |
530
+ |-------|----------|--------|
531
+ | `MarantzReceiver` | 2015 IP/RS-232 (`PREFIX+VALUE\r`) | NR1506, NR1606, SR5010, SR6010, SR7010, AV7702mkII |
532
+ | `MarantzLegacyReceiver` | 2007 RS-232 (`@CMD:VALUE\r`) | SR7002, SR8002, SR6003, SR7003, SR8003, SR5004, SR6004, AV7005, AV8003 |
533
+
534
+ 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.
535
+
536
+ ## Development
537
+
538
+ ```bash
539
+ # Install dev dependencies
540
+ uv sync
541
+
542
+ # Run tests
543
+ uv run pytest
544
+
545
+ # Run tests with verbose output
546
+ uv run pytest -v
547
+ ```
548
+
549
+ ## License
550
+
551
+ MIT