Adding a Device Profile¶
This guide shows how to add support for a new RS232 audio device. Most devices can be added with a single YAML file — no Python code required.
Quick Start¶
Using a device that isn't bundled (no code, survives updates):
- Copy
_template.yamlinto your Home Assistant config asconfig/audio_control/<manufacturer>_<model>.yaml(create theaudio_controlfolder if needed). - Fill it in from the device's RS232 manual (format below).
- Restart Home Assistant. The profile appears in the Device dropdown with a
(Custom)suffix. - Try it first with the connection set to
demo, then switch to the real port via Reconfigure.
Do not edit the bundled profiles/ folder on an installed copy: HACS
replaces it on every update.
Contributing a profile to the bundle: add the file to
custom_components/audio_control/rs232_protocol/profiles/, validate it (see
Validation), and open a PR.
Profile Template¶
# manufacturer_model.yaml
name: "Manufacturer Model Name"
manufacturer: "Manufacturer"
models: ["Model1", "Model2"]
zones: 6
sources: 8
serial:
baud_rate: 9600
data_bits: 8
parity: "none" # none, even, odd
stop_bits: 1
command_delay_ms: 100 # minimum ms between commands
command_prefix: "" # prepended to every command (e.g., "*", "!")
command_suffix: "\r" # appended to every command
response_prefix: "" # stripped from responses (e.g., "#", ">")
response_terminator: "\r" # end-of-response delimiter
commands:
status: "Z{zone:02d}STA"
power_on: "Z{zone:02d}PON"
power_off: "Z{zone:02d}POFF"
volume_set: "Z{zone:02d}VOL{volume:02d}"
volume_up: "Z{zone:02d}VU"
volume_down: "Z{zone:02d}VD"
mute_on: "Z{zone:02d}MUON"
mute_off: "Z{zone:02d}MUOFF"
source_set: "Z{zone:02d}SRC{source:02d}"
# Optional:
bass_set: "Z{zone:02d}BAS{sign}{level}"
treble_set: "Z{zone:02d}TRB{sign}{level}"
all_off: "ALLOFF"
group_on: "GRP{source:02d}"
group_off: "GRPOFF"
response:
status_pattern: "Z(?P<zone>\\d{2}),PWR(?P<power>\\d),SRC(?P<source>\\d{2}),VOL(?P<volume>\\d{2}),MUT(?P<mute>\\d)"
error_response: "?"
# Optional:
tone_pattern: "Z(?P<zone>\\d{2}),BASS(?P<bass>[+-]?\\d+),TREB(?P<treble>[+-]?\\d+)"
global_patterns:
all_off: "ALLOFF"
group_on: "GRP(?P<source>\\d{2})"
mapping:
power:
field: "power"
type: "bool"
on_value: "1"
mute:
field: "mute"
type: "bool"
on_value: "1"
volume:
field: "volume"
type: "int"
source:
field: "source"
type: "int"
# Optional tone mappings:
bass:
field: "bass"
type: "signed_int"
treble:
field: "treble"
type: "signed_int"
# Optional: tone limits the device enforces (rejected before sending, with a
# clear message, instead of a failed command).
tone:
levels: [-8, -6, -4, 0, 4, 6, 8] # omit for the default even steps -8..+8; range -12..+12
requires_power: true # device refuses tone on an OFF zone
volume:
min: 0
max: 38
inverted: false # true if 0=loud, max=mute (like AMC's attenuation)
features:
has_tone_controls: false
has_group_mode: false
has_all_off: false
has_balance: false
unsolicited_updates: false # true if device pushes state without polling
Command Placeholders¶
| Placeholder | Description | Example |
|---|---|---|
{zone} |
Zone number (1-based) | 1, 2, ... 16 |
{zone:02d} |
Zone zero-padded to 2 digits | 01, 02, ... 16 |
{zone:1d} |
Zone as single digit | 1, 2, ... 8 |
{source} |
Source number (1-based) | 1, 2, ... 8 |
{source:02d} |
Source zero-padded | 01, 02, ... 16 |
{volume} |
Volume value (device units) | 0-38, 0-62, 0-100 |
{volume:02d} |
Volume zero-padded | 00-62 |
{level} |
Tone level (bass/treble) | 0-14, or absolute |
{sign} |
Sign character for tone | + or - |
{source_name} |
Named source (for Rotel/Marantz) | cd, aux1 |
Response Mapping Types¶
| Type | Description | Example |
|---|---|---|
int |
Parse as integer | "25" → 25 |
signed_int |
Signed integer (for +/- values) | "-4" → -4 |
bool |
Compare to on_value string |
"1" == "1" → True |
string_bool |
String comparison | "ON" == "ON" → True |
threshold |
True if value != off_value |
25 != 62 → True (power on) |
equals |
True if value == mute_value |
62 == 62 → True (muted) |
string |
Keep as string | "cd" → "cd" |
Add offset: -7 to shift raw values (e.g., Xantech bass 0-14 → -7 to +7).
Add negative_base: 20 to a signed_int tone mapping for devices that encode a cut as base + magnitude (AMC XOir: 22 = −2 dB); the same key on the mapping drives encoding of bass_set/treble_set with a {level:02d} template.
For JSON profiles, scope: device on a response.mappings entry marks a reply that describes the whole unit (RTI set_powerstate): it is applied to every zone.
Add skip_values: [0] to ignore raw values that carry no state for that key — e.g. AMC reports SRC00 for an OFF zone, which must not become "Source 0" (power still reads the same field via threshold).
Make fields that a device sometimes omits optional in the pattern, e.g. (?:,GRP(?P<group>\\d))? — an optional group that doesn't match is skipped, not mapped to a default. Base patterns on replies captured from real hardware: the AMC manual shows ,GRP in the tone reply, but a real X86i leaves it out.
Validation¶
From a checkout of this repository (make setup once):
# Every bundled profile must load and format its commands
.venv/bin/python -m pytest tests/audio_control/test_profile_loader.py -q
# Or poke at one profile directly:
PYTHONPATH=custom_components/audio_control .venv/bin/python -c "
from rs232_protocol import create_engine, load_builtin_profiles
engine = create_engine(load_builtin_profiles()['Your Device Name'])
print(engine.format_command('power_on', zone=1))
print(engine.format_command('volume_set', zone=1, volume=25))
"
Where to Find Protocol Info¶
| Source | What you'll find |
|---|---|
| Device manual (PDF) | RS232 appendix with command table |
| Manufacturer website | Support/downloads section |
| GitHub search | <manufacturer> rs232 — often has protocol docs |
| Control4/Crestron driver DBs | Protocol specs embedded in drivers |
| Home automation forums | Community reverse-engineering |
pypi.org search |
Python libraries with protocol details |
Protocol Types¶
Most audio devices use the default text protocol. For binary or JSON protocols, see protocol-types.md.
Submitting¶
- Name your file:
manufacturer_model.yaml(lowercase, underscores) - Include a comment header with the full device name
- List all compatible models in the
models:array - Cite the manual (and ideally confirm on real hardware) in the PR
- Run
make cito verify all checks pass, then open a PR