anthem-rs232 0.2.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.
- anthem_rs232-0.2.0/PKG-INFO +430 -0
- anthem_rs232-0.2.0/README.md +409 -0
- anthem_rs232-0.2.0/pyproject.toml +46 -0
- anthem_rs232-0.2.0/src/anthem_rs232/__init__.py +143 -0
- anthem_rs232-0.2.0/src/anthem_rs232/__main__.py +257 -0
- anthem_rs232-0.2.0/src/anthem_rs232/_runtime.py +154 -0
- anthem_rs232-0.2.0/src/anthem_rs232/const.py +234 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/__init__.py +127 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/const.py +187 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/models.py +201 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/protocol.py +262 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/receiver.py +1000 -0
- anthem_rs232-0.2.0/src/anthem_rs232/gen1/state.py +155 -0
- anthem_rs232-0.2.0/src/anthem_rs232/models.py +182 -0
- anthem_rs232-0.2.0/src/anthem_rs232/players.py +340 -0
- anthem_rs232-0.2.0/src/anthem_rs232/probe.py +273 -0
- anthem_rs232-0.2.0/src/anthem_rs232/protocol.py +116 -0
- anthem_rs232-0.2.0/src/anthem_rs232/py.typed +0 -0
- anthem_rs232-0.2.0/src/anthem_rs232/receiver.py +789 -0
- anthem_rs232-0.2.0/src/anthem_rs232/state.py +133 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anthem-rs232
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async library to control Anthem MRX 1120 / 720 / 520 / AVM 60 receivers over RS232
|
|
5
|
+
Author: Ben Hass
|
|
6
|
+
Author-email: Ben Hass <ben@groovie.org>
|
|
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.14
|
|
13
|
+
Classifier: Topic :: Home Automation
|
|
14
|
+
Classifier: Topic :: System :: Hardware
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Requires-Dist: serialx[esphome]>=1.8.2
|
|
17
|
+
Requires-Dist: serial-toolkit>=0.2.0
|
|
18
|
+
Requires-Python: >=3.14
|
|
19
|
+
Project-URL: Repository, https://github.com/bbangert/anthem-rs232
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# anthem-rs232
|
|
23
|
+
|
|
24
|
+
Async Python library for the Anthem RS-232 protocol family — covers the **Gen 2** receivers (Statement-replacement MRX 310 through MRX 1120, AVM 60) and the older **Gen 1** receivers (Statement D1/D2/D2v, AVM 20–50, MRX 300/500/700). Built on [serialx](https://github.com/puddly/serialx).
|
|
25
|
+
|
|
26
|
+
Modeled on [denon-rs232](https://github.com/home-assistant-libs/denon-rs232); Gen 1 command set ported from [rsnodgrass/python-anthemav-serial](https://github.com/rsnodgrass/python-anthemav-serial).
|
|
27
|
+
|
|
28
|
+
## Supported hardware
|
|
29
|
+
|
|
30
|
+
| Model(s) | Series | Constants | RS232 |
|
|
31
|
+
|----------------------------|-------------|------------------------------------|-------|
|
|
32
|
+
| Statement D1 | d1 | `STATEMENT_D1` | Gen 1 |
|
|
33
|
+
| Statement D2, D2v | d2 / d2v | `STATEMENT_D2`, `STATEMENT_D2V` | Gen 1 |
|
|
34
|
+
| AVM 20, AVM 30, AVM 40 | avm20–avm40 | `AVM_20`, `AVM_30`, `AVM_40` | Gen 1 |
|
|
35
|
+
| AVM 50, AVM 50v | avm50 | `AVM_50`, `AVM_50V` | Gen 1 |
|
|
36
|
+
| MRX 300, MRX 500, MRX 700 | mrx | `MRX_300`, `MRX_500`, `MRX_700` | Gen 1 |
|
|
37
|
+
| MRX 310, MRX 510, MRX 710 | mrx1 | `MRX_310`, `MRX_510`, `MRX_710` | Gen 2 |
|
|
38
|
+
| MRX 520, MRX 720, MRX 1120 | mrx2 | `MRX_520`, `MRX_720`, `MRX_1120` | Gen 2 |
|
|
39
|
+
| AVM 60 | avm60 | `AVM_60` | Gen 2 |
|
|
40
|
+
|
|
41
|
+
Gen 1 constants live in `anthem_rs232.gen1`; Gen 2 in `anthem_rs232.models`.
|
|
42
|
+
|
|
43
|
+
The two generations use different wire formats and have separate receiver classes:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# Gen 2 (semicolon-terminated, 115200 baud)
|
|
47
|
+
from anthem_rs232 import AnthemReceiver
|
|
48
|
+
from anthem_rs232.models import MRX_1120
|
|
49
|
+
gen2 = AnthemReceiver("/dev/ttyUSB0", model=MRX_1120)
|
|
50
|
+
|
|
51
|
+
# Gen 1 (LF-terminated, 9600 / 19200 baud per model)
|
|
52
|
+
from anthem_rs232 import Gen1Receiver
|
|
53
|
+
from anthem_rs232.gen1 import STATEMENT_D2
|
|
54
|
+
gen1 = Gen1Receiver("/dev/ttyUSB0", model=STATEMENT_D2)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Not supported
|
|
58
|
+
|
|
59
|
+
- **STR amplifiers** — Gen 2 protocol family but not validated against this library.
|
|
60
|
+
- **Gen 4** — MRX 540 / 740 / 1140 + AVM 70 / 90. Substantially different protocol (new `GC*`, `NM*`, `SS*`, split `Z1ARC*` sub-commands; tuner removed).
|
|
61
|
+
- **MRX SLM** (2024+) — IP-only.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install anthem-rs232
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requires Python 3.12+.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
from anthem_rs232 import AnthemReceiver, AudioListeningMode
|
|
76
|
+
|
|
77
|
+
async def main():
|
|
78
|
+
receiver = AnthemReceiver("/dev/ttyUSB0")
|
|
79
|
+
await receiver.connect()
|
|
80
|
+
await receiver.query_state()
|
|
81
|
+
|
|
82
|
+
print(f"Model: {receiver.state.model}")
|
|
83
|
+
print(f"Power: {receiver.state.power}")
|
|
84
|
+
print(f"Volume: {receiver.state.main_zone.volume} dB")
|
|
85
|
+
print(f"Input: {receiver.state.main_zone.input_index}")
|
|
86
|
+
|
|
87
|
+
await receiver.main.set_volume(-30.0)
|
|
88
|
+
await receiver.main.select_input(2)
|
|
89
|
+
await receiver.main.set_audio_listening_mode(AudioListeningMode.DOLBY_SURROUND)
|
|
90
|
+
|
|
91
|
+
await receiver.disconnect()
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## CLI
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Detect which Anthem model + protocol generation is on the wire (auto-tries
|
|
100
|
+
# Gen 2 @ 115200, Gen 1 @ 9600, Gen 1 @ 19200)
|
|
101
|
+
python -m anthem_rs232.probe /dev/ttyUSB0
|
|
102
|
+
|
|
103
|
+
# Query and print receiver status (Gen 2)
|
|
104
|
+
python -m anthem_rs232 /dev/ttyUSB0
|
|
105
|
+
|
|
106
|
+
# Probe configured inputs (uses ICN? + ISN?/ILN?)
|
|
107
|
+
python -m anthem_rs232 /dev/ttyUSB0 --probe
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Detecting the model
|
|
111
|
+
|
|
112
|
+
If you don't know which receiver is on a serial port (or which generation), use `probe()`:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from anthem_rs232 import probe
|
|
116
|
+
|
|
117
|
+
result = await probe("/dev/ttyUSB0")
|
|
118
|
+
# result.generation -> 1 or 2
|
|
119
|
+
# result.model_name -> "MRX 1120"
|
|
120
|
+
# result.baud_rate -> 115200
|
|
121
|
+
# result.model -> the matching model constant (MRX_1120) or None
|
|
122
|
+
|
|
123
|
+
if result is None:
|
|
124
|
+
print("No Anthem responded")
|
|
125
|
+
else:
|
|
126
|
+
print(f"{result.model_name} (Gen {result.generation}, {result.baud_rate} baud)")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The probe walks Gen 2 (115200, `IDM?` then `Z1POW?` fallback) → Gen 1 @ 9600 (`?\n`) → Gen 1 @ 19200, and returns the first match. Works against the same port strings the receivers accept (local devices, `socket://`, `esphome://`).
|
|
130
|
+
|
|
131
|
+
## Features
|
|
132
|
+
|
|
133
|
+
### Full state after query
|
|
134
|
+
|
|
135
|
+
`connect()` opens the serial port, verifies the link with `IDM?`, and enables auto-reports with `ECH1`. Call `query_state()` to populate the current receiver state into the `state` property — after that, state stays current via auto-report events from the receiver.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
receiver = AnthemReceiver("/dev/ttyUSB0")
|
|
139
|
+
await receiver.connect()
|
|
140
|
+
await receiver.query_state()
|
|
141
|
+
|
|
142
|
+
state = receiver.state
|
|
143
|
+
state.model # "MRX 1120"
|
|
144
|
+
state.software_version # "0.2.3"
|
|
145
|
+
state.power # True if any zone is on
|
|
146
|
+
state.front_panel_brightness # FrontPanelBrightness.MEDIUM
|
|
147
|
+
state.inputs # {1: InputConfig(...), ...}
|
|
148
|
+
|
|
149
|
+
mz = state.main_zone
|
|
150
|
+
mz.power # True / False
|
|
151
|
+
mz.input_index # 1
|
|
152
|
+
mz.volume # -35.0
|
|
153
|
+
mz.mute # True / False
|
|
154
|
+
mz.arc_enabled # True / False
|
|
155
|
+
mz.balance # 0-100, 50 = center
|
|
156
|
+
mz.audio_listening_mode # AudioListeningMode enum
|
|
157
|
+
mz.audio_input_format # AudioInputFormat enum
|
|
158
|
+
mz.video_input_resolution # VideoInputResolution enum
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Event subscription
|
|
162
|
+
|
|
163
|
+
Subscribe to state changes. Callbacks receive a `ReceiverState` snapshot on updates, or `None` when the connection drops.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
def on_change(state):
|
|
167
|
+
if state is None:
|
|
168
|
+
print("Disconnected!")
|
|
169
|
+
return
|
|
170
|
+
print(f"Volume: {state.main_zone.volume} dB")
|
|
171
|
+
|
|
172
|
+
unsub = receiver.subscribe(on_change)
|
|
173
|
+
unsub() # stop receiving
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Power
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
await receiver.power_on_all() # Z0POW1 -- power all zones on
|
|
180
|
+
await receiver.power_off_all() # Z0POW0 -- system standby
|
|
181
|
+
await receiver.main.power_on() # Z1POW1
|
|
182
|
+
await receiver.main.power_off() # Z1POW0
|
|
183
|
+
on = await receiver.main.query_power()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Volume
|
|
187
|
+
|
|
188
|
+
Volume is in dB and rounded to the nearest valid value by the receiver.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
await receiver.main.set_volume(-30.0) # Z1VOL-30
|
|
192
|
+
await receiver.main.set_volume(5.0) # Z1VOL+05
|
|
193
|
+
await receiver.main.volume_up() # Z1VUP01 (1 dB step)
|
|
194
|
+
await receiver.main.volume_up(step=5) # Z1VUP05
|
|
195
|
+
await receiver.main.volume_down(step=2)
|
|
196
|
+
db = await receiver.main.query_volume()
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Mute
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
await receiver.main.mute_on()
|
|
203
|
+
await receiver.main.mute_off()
|
|
204
|
+
await receiver.main.mute_toggle()
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Inputs
|
|
208
|
+
|
|
209
|
+
Anthem inputs are configurable and identified by 1-based index. Names come from `ISN?` (short) and `ILN?` (long) and are populated during `query_state()`.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
await receiver.main.select_input(2)
|
|
213
|
+
idx = await receiver.main.query_input()
|
|
214
|
+
|
|
215
|
+
inputs = receiver.state.inputs
|
|
216
|
+
print(inputs[1].short_name) # "CBL"
|
|
217
|
+
print(inputs[1].long_name) # "Cable Box"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Audio listening mode
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from anthem_rs232 import AudioListeningMode
|
|
224
|
+
|
|
225
|
+
await receiver.main.set_audio_listening_mode(AudioListeningMode.DOLBY_SURROUND)
|
|
226
|
+
await receiver.main.audio_listening_mode_next()
|
|
227
|
+
mode = await receiver.main.query_audio_listening_mode()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Anthem Room Correction
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
await receiver.main.arc_on()
|
|
234
|
+
await receiver.main.arc_off()
|
|
235
|
+
on = await receiver.main.query_arc()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Per-input processing
|
|
239
|
+
|
|
240
|
+
Lip sync delay, Dolby Volume, and the Dolby Volume Leveler are stored per
|
|
241
|
+
input on the receiver. ``input_index=0`` (the default) targets the currently
|
|
242
|
+
selected input.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
await receiver.set_lip_sync(50) # SLIP00050 (0-150 ms, 5 ms steps)
|
|
246
|
+
await receiver.set_dolby_volume(True, input_index=2) # SDVS021
|
|
247
|
+
await receiver.set_dolby_volume_leveler(5) # SDVL005 (0 = off, 1-9)
|
|
248
|
+
|
|
249
|
+
ms = await receiver.query_lip_sync()
|
|
250
|
+
on = await receiver.query_dolby_volume()
|
|
251
|
+
level = await receiver.query_dolby_volume_leveler()
|
|
252
|
+
|
|
253
|
+
# Populated per input from query replies and auto-reports:
|
|
254
|
+
receiver.state.inputs[2].lip_sync_ms
|
|
255
|
+
receiver.state.inputs[2].dolby_volume
|
|
256
|
+
receiver.state.inputs[2].dolby_volume_leveler
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Balance, channel levels, tone
|
|
260
|
+
|
|
261
|
+
These are runtime adjustments meant to compensate for source material — for system setup, use the receiver's setup menu and ARC.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from anthem_rs232 import Channel
|
|
265
|
+
|
|
266
|
+
await receiver.main.set_balance(50) # 0=full left, 100=full right
|
|
267
|
+
await receiver.main.set_channel_level(Channel.SUBS, -3.0)
|
|
268
|
+
await receiver.main.set_channel_level(Channel.HEIGHTS_1, 2.0)
|
|
269
|
+
await receiver.main.channel_level_up(Channel.CENTER)
|
|
270
|
+
|
|
271
|
+
await receiver.main.set_bass(-2.0)
|
|
272
|
+
await receiver.main.set_treble(3.0)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Tuner (FM)
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
await receiver.main.set_fm_frequency(105.50) # T1FMS105.50
|
|
279
|
+
await receiver.main.tune_up()
|
|
280
|
+
await receiver.main.seek_up()
|
|
281
|
+
await receiver.main.preset_up()
|
|
282
|
+
await receiver.main.select_preset(7)
|
|
283
|
+
await receiver.main.assign_preset(7)
|
|
284
|
+
await receiver.main.remove_preset(7)
|
|
285
|
+
freq = await receiver.main.query_fm_frequency()
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Zone 2
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
await receiver.zone_2.power_on()
|
|
292
|
+
await receiver.zone_2.select_input(2)
|
|
293
|
+
await receiver.zone_2.set_volume(-40.0)
|
|
294
|
+
await receiver.zone_2.mute_on()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Setup / display
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from anthem_rs232 import FrontPanelBrightness
|
|
301
|
+
|
|
302
|
+
await receiver.set_front_panel_brightness(FrontPanelBrightness.HIGH)
|
|
303
|
+
await receiver.set_standby_ip_control(True) # required for IP power-on
|
|
304
|
+
await receiver.set_speaker_profile(2)
|
|
305
|
+
await receiver.main.display_message(0, "Hello")
|
|
306
|
+
await receiver.main.open_setup_menu()
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Triggers
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
await receiver.set_trigger_control(1, rs232_controlled=True)
|
|
313
|
+
await receiver.set_trigger(1, on=True)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Connection handling
|
|
317
|
+
|
|
318
|
+
- If `IDM?` is not answered during `connect()`, `ConnectionError` is raised.
|
|
319
|
+
- If the serial connection drops, subscribers receive `None` and `connected` becomes `False`.
|
|
320
|
+
- An idle watchdog probes the link (`Z1POW?`, answered even in standby) after 60 s without RX and tears the connection down if repeated probes go unanswered — covering transports (e.g. serial-over-network proxies) that can die without delivering EOF. The probe retries up to 3 times because a unit in ECO standby consumes the first frame as its wake-up.
|
|
321
|
+
- Receiver errors come back as `!E/R/I/Z<original>;` and raise `CommandError` from the matching pending query. Fire-and-forget commands log the error.
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
try:
|
|
325
|
+
await receiver.connect()
|
|
326
|
+
except ConnectionError:
|
|
327
|
+
print("Receiver not responding")
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Gen 1 (Statement / older AVM / older MRX)
|
|
331
|
+
|
|
332
|
+
The Gen 1 receiver lives in the `gen1` subpackage and exposes a different API surface (because the protocol is different — different command shape, different volume scale, dedicated headphone output, decoder/effect modes, channel trims).
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
import asyncio
|
|
336
|
+
from anthem_rs232 import Gen1Receiver
|
|
337
|
+
from anthem_rs232.gen1 import (
|
|
338
|
+
STATEMENT_D2, Source, DecoderMode, EffectMode, SleepTimer,
|
|
339
|
+
DolbyDynamicRange, TunerMode,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
async def main():
|
|
343
|
+
rx = Gen1Receiver("/dev/ttyUSB0", model=STATEMENT_D2)
|
|
344
|
+
await rx.connect() # opens serial, sends ?, enables SST1 auto-reports
|
|
345
|
+
await rx.query_state() # P1?, P2?, P3?, H?, TT?
|
|
346
|
+
|
|
347
|
+
state = rx.state
|
|
348
|
+
print(state.model, state.version) # "Statement D2" "3.10"
|
|
349
|
+
print(state.main_zone.volume) # e.g. -30.5
|
|
350
|
+
|
|
351
|
+
# Main zone (P1)
|
|
352
|
+
await rx.main.power_on() # P1P1
|
|
353
|
+
await rx.main.set_volume(-30.5) # P1VM-30.5 (0.5 dB step)
|
|
354
|
+
await rx.main.mute_toggle() # P1MT
|
|
355
|
+
await rx.main.select_source(Source.DVD_1.value) # P1S5
|
|
356
|
+
await rx.main.set_decoder_mode("5", DecoderMode.PRO_LOGIC)
|
|
357
|
+
await rx.main.set_effect_mode("5", EffectMode.HALL)
|
|
358
|
+
await rx.main.set_dolby_dynamic_range(DolbyDynamicRange.LATE_NIGHT)
|
|
359
|
+
await rx.main.set_sleep_timer(SleepTimer.SIXTY_MIN)
|
|
360
|
+
await rx.main.display_message(1, "Hello")
|
|
361
|
+
|
|
362
|
+
# Channel trims and balance (Main only)
|
|
363
|
+
await rx.main.set_center_trim(1.5)
|
|
364
|
+
await rx.main.set_sub_trim(-2.0)
|
|
365
|
+
await rx.main.set_front_balance(2.0)
|
|
366
|
+
await rx.main.set_master_bass(-3.0)
|
|
367
|
+
|
|
368
|
+
# Zone 2 (P2) -- separate volume scale (1.25 dB step)
|
|
369
|
+
await rx.zone_2.power_on()
|
|
370
|
+
await rx.zone_2.set_volume(-50.0) # P2V-50.0 (no "M" after V)
|
|
371
|
+
await rx.zone_2.set_balance(2.5)
|
|
372
|
+
await rx.zone_2.set_treble(4.0)
|
|
373
|
+
|
|
374
|
+
# Rec / Zone 3 (P3) -- source-only
|
|
375
|
+
await rx.rec.select_source(Source.TUNER.value)
|
|
376
|
+
|
|
377
|
+
# Headphone (H) -- dedicated output
|
|
378
|
+
await rx.headphone.set_volume(-20.0)
|
|
379
|
+
await rx.headphone.mute_toggle()
|
|
380
|
+
|
|
381
|
+
# Tuner (T)
|
|
382
|
+
await rx.tuner.set_fm_frequency(101.5) # TFT101.5
|
|
383
|
+
await rx.tuner.set_am_frequency(540) # TAT0540
|
|
384
|
+
await rx.tuner.fm_preset(1, 2) # TFP12
|
|
385
|
+
band, freq = await rx.tuner.query_frequency()
|
|
386
|
+
|
|
387
|
+
# 12 V triggers
|
|
388
|
+
await rx.set_trigger(1, on=True) # t1T1
|
|
389
|
+
|
|
390
|
+
# System
|
|
391
|
+
await rx.power_on_all() # P1P1;P2P1;P3P1
|
|
392
|
+
await rx.lock_front_panel() # FPL1
|
|
393
|
+
await rx.rename_source("5", "Apple") # SN5Apple
|
|
394
|
+
await rx.save_user_settings() # SfSU
|
|
395
|
+
|
|
396
|
+
await rx.disconnect()
|
|
397
|
+
|
|
398
|
+
asyncio.run(main())
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Gen 1 notes
|
|
402
|
+
|
|
403
|
+
- **Wire format**: LF-terminated ASCII, default 9600 baud (D2/D2v: 19200). Multiple commands can be chained on one line with `;`.
|
|
404
|
+
- **No reply on success**: the Gen 1 receiver answers queries only — set commands are silent unless they fail. Connect enables Tx Status (`SST1`) so external state changes (front panel, IR remote, knob, bitstream change) propagate as auto-report frames.
|
|
405
|
+
- **Errors are verbose ASCII**: `Invalid Command`, `Parameter Out-of-range`, `Main Off`, `Zone2 Off`, `Unit Off`, `Already in use`. They raise `Gen1CommandError` from the matching pending query.
|
|
406
|
+
- **Volume**: dB float with one decimal place. Main zone uses 0.5 dB step (`P1VM`); Zone 2 / Headphone use 1.25 dB step (`P2V` / `HV`). The library snaps to the nearest grid.
|
|
407
|
+
- **Source codes**: single ASCII characters (`0`–`9`, plus `d`–`j` on D2/D2v for second-bank inputs). The `Source` enum lists them.
|
|
408
|
+
- **Identify**: `?` returns `(model,version,build)` — populated into `state.model` / `state.version` / `state.build_date` during `connect()`.
|
|
409
|
+
|
|
410
|
+
## Serial connection
|
|
411
|
+
|
|
412
|
+
**Gen 2** (MRX 310+, AVM 60): 115200 baud, 8N1, no flow control, `;`-terminated.
|
|
413
|
+
**Gen 1** (Statement, older AVM/MRX): 9600 or 19200 baud (per model), 8N1, no flow control, LF-terminated.
|
|
414
|
+
Both use a straight-wired DB-9 cable (Pin 2 = Tx, 3 = Rx, 5 = GND).
|
|
415
|
+
|
|
416
|
+
## Protocol references
|
|
417
|
+
|
|
418
|
+
- Gen 2: [Anthem MRX 1120 / 720 / 520 / AVM 60 IP / RS-232 serial commands (Google Sheets)](https://docs.google.com/spreadsheets/d/1_lN8JWSIPRrWrqxuZ3lNGy5GQ3FjyaBi6kIeuk21GP4/edit?usp=sharing)
|
|
419
|
+
- Gen 1: AVM-2 serial spec (21 November 2000) — bundled in the [rsnodgrass/python-anthemav-serial repo](https://github.com/rsnodgrass/python-anthemav-serial/blob/main/docs/AVM-2-serial-programming.txt)
|
|
420
|
+
|
|
421
|
+
## Development
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
uv sync
|
|
425
|
+
uv run pytest
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
MIT
|