I was tired of digging through tiny on-device menus just to turn Zone 2 on and off on my AVR.
So I built a small Go CLI (zone2) that talks directly to the receiver over WebSocket, then wired it into Home Assistant as a command_line switch.
If you have an Arcam / AudioControl / JBL Synthesis receiver that exposes the setup WebSocket API on port 50001, this approach is straightforward and fast.
Why I chose a CLI first
Instead of jumping straight into a custom Home Assistant integration, I wanted a simple binary that:
- can run anywhere
- is easy to test from terminal
- returns plain
on/offoutput for automation parsing
That gave me a stable foundation before touching dashboards and YAML.
How the AVR communication works
The tool connects to:
ws://<AVR_IP>:50001
It sends binary protocol frames for Zone 2 (command 0x2F), then parses binary responses from the AVR.
Frame format (simplified):
request: 21 01 <command> <len> <payload...> 0D
response: 21 01 <command> <status> <len> <payload...> 0D
For reads, it queries the Zone 2 model and inspects the second byte (model[1]) as power state:
-
0=> off -
1=> on
For writes (on, off, toggle), it updates that byte and sends the full model back.
Reliability changes that mattered
The hard part was not "sending commands", it was making behavior reliable enough for automations.
I added a few guardrails:
-
Frame splitting
- Receivers can send multiple protocol frames in a single WebSocket message.
- The client extracts valid frames by header/length/terminator and picks the right command.
-
Strict response parsing
- Validates framing, command ID, terminator, and declared payload length.
- Rejects malformed frames early.
-
Retry on transient states
- Zone 2 query retries up to 5 times.
- Handles temporary
0x85status by waiting briefly and retrying.
-
Post-write verification loop
- After a write, it re-queries state until the target sticks (default
20attempts). - This avoids false positives where a write ACK arrives but state does not actually change.
- After a write, it re-queries state until the target sticks (default
This is what made it usable from Home Assistant without random "it worked... I think?" moments.
CLI usage
./zone2-macos-arm64 -host YOUR_AVR_IP -mode status
./zone2-macos-arm64 -host YOUR_AVR_IP -mode on
./zone2-macos-arm64 -host YOUR_AVR_IP -mode off
./zone2-macos-arm64 -host YOUR_AVR_IP -mode toggle
Flags:
-
-host(required) -
-mode(on|off|toggle|status) -
-timeout(default4s) -
-verify(default20, used for writes) -
-verbose(prints raw TX/RX frames)
Home Assistant integration
I cross-compile a Linux ARM64 binary for HA OS (Raspberry Pi 5), copy it into /config/bin, and expose it via command_line:
command_line:
- switch:
name: AVR Zone 2
unique_id: avr_zone2
command_on: "/config/bin/zone2 -host YOUR_AVR_IP -mode on -timeout 4s -verify 20"
command_off: "/config/bin/zone2 -host YOUR_AVR_IP -mode off -timeout 4s -verify 20"
command_state: "/config/bin/zone2 -host YOUR_AVR_IP -mode status -timeout 4s"
value_template: "{{ value | trim | lower == 'on' }}"
scan_interval: 10
Because the CLI prints exactly on or off, the value template stays simple and reliable.
Build + release flow
The repo includes:
- Makefile targets for Linux ARM64 + macOS binaries
- CI (
go test,go vet) - tag-triggered GitHub release workflow attaching compiled binaries
That made it easy to test locally and deploy repeatably.
Final thoughts
This was a small project, but a useful reminder: in home automation, state verification is more important than "command sent successfully".
A tiny, deterministic CLI with clear output is often enough to bridge unsupported hardware into Home Assistant cleanly.
If I extend this next, I'll likely add controls for input/volume and package a full HA integration once the protocol surface is mature.
Top comments (0)