Automating Denon AVR zone switching

My home audio system goes through a Denon AVR-X1700H 1 This regularly goes on sale, so definitely don't pay more than $400, and you can get it substantially cheaper. as the primary receiver. My house has three pairs of ceiling speakers: one in the living room, one in the dining room, and one in the kitchen. 2 The previous owners seemingly never used the ceiling speakers: when I moved in I asked how they were wired and they said they had no idea and never turned them on. Baffling. While the X1700H only has two zones, I really only use the speakers for TV audio (just the living room) or general music (all three), so wiring the dining room and kitchen together into Zone 2 was enough.

The receiver has a setting called "All Zone Stereo" which broadcasts the same output to Zone 1 and Zone 2, which is perfect for playing music. Unfortunately as far as I can tell there's no way to turn that setting on from the physical receiver, requiring either a convoluted series of steps on the remote or the somewhat crappy Denon AVR app:

It's not responsive until that happens, and this process takes around 2s every time because it never caches what it's going to show.
First open the app and click Options. Then you wait... ...until every option loads. 3 Select All Zone Stereo and click Start.

Let's bypass the app.

Becoming a middleman to remove the middleman

We can follow my previous guide for mitmproxy 4 One of these days I'll turn this into a Snippet so I can stop linking to the Letterboxd post. to snoop on the requests that are being made by the app. Note that you'll have to connect to the proxy after the initial connection to the receiver is made, or it will hang (likely because it's phoning back to a Denon core server that actually has certificate pinning).

Once we're there we start seeing a flood of POST requests to /goform/AppCommand.xml and /goform/AppCommand0300.xml on port 8080. Despite being POST requests most of these are "GET"ing data, with commands like GetAllZoneStereo and GetAllZoneSource being hit once a second to keep the UI updated.

GetAllZoneStereo GetAllZonePowerStatus

However there are also the commands to change inputs, volumes, and most specifically for me, the SetAllZoneStereo.

SetAllZoneVolume SetAllZoneStereo (on) SetAllZoneStereo (off)

For each of these you POST a pretty simple XML 5 Extensible Markup Language, which beats a lot of other serialization formats by actually allowing <!-- comments --> (cough looking at you JSON cough) payload, like this to the URL in question:

<?xml version="1.0" encoding="utf-8"?>
<tx>
  <cmd id="1">GetAllZoneStereo</cmd>
</tx>

and get a response:

<?xml version="1.0" encoding="utf-8" ?>
<rx>
  <cmd>
    <status>1</status>
    <value>0</value>
    <zones>000</zones>
    <selections>100</selections>
  </cmd>
</rx>

The cmd id='1' in the payload is for /goform/AppCommand.xml, whereas it's id='3' for AppCommand0300.xml (corroborated by these scrapes on GitHub). I'm not sure if there's a hard rule for which goes to which, but SetAllZoneVolume was the only one pointing to AppCommand0300.xml out of what I used.

Now all you need to do is curl the endpoint from within your network with a Content-Type and the string payload. Here's a sample command that enables "All Zone Stereo":

curl -X POST 'http://192.168.0.243:8080/goform/AppCommand.xml' \
  -H 'Content-Type: text/xml; charset="utf-8"' \
  --data-binary $'<?xml version="1.0" encoding="utf-8"?>
<tx>
  <cmd id="1">SetAllZoneStereo</cmd>
  <status>1</status>
  <zones>100</zones>
</tx>'

If you want to see the rest of the commands, I put all the payloads and responses in this public Gist.

Putting it all together

The point of all of this is simple — to play Switch Sports Golf. 6 If they added support for community maps it might be the greatest game of all time.

While playing it on the TV I want to be able to route Spotify to Zone 2, but I want both zones to Spotify when I'm listening to music, and switching is annoying. Luckily with this reverse engineering I could trivially hook it into my personal home automation app (stemming form a system I built in college called Victor that I've blogged about previously), and have it live right on my Apple Watch. 7 Thanks to SF Symbol's figure.golf, 􁔩, which might render for you on a Mac and certainly doesn't render anywhere else (it uses U+101529 which lives in the private use section, and relies on "SF Pro").

watch

Now watch this drive.


  1. This regularly goes on sale, so definitely don't pay more than $400, and you can get it substantially cheaper. ↩︎

  2. The previous owners seemingly never used the ceiling speakers: when I moved in I asked how they were wired and they said they had no idea and never turned them on. Baffling. ↩︎

  3. It's not responsive until that happens, and this process takes around 2s every time because it never caches what it's going to show. ↩︎

  4. One of these days I'll turn this into a Snippet so I can stop linking to the Letterboxd post. ↩︎

  5. Extensible Markup Language, which beats a lot of other serialization formats by actually allowing <!-- comments --> (cough looking at you JSON cough↩︎

  6. If they added support for community maps it might be the greatest game of all time. ↩︎

  7. Thanks to SF Symbol's figure.golf, 􁔩, which might render for you on a Mac and certainly doesn't render anywhere else (it uses U+101529 which lives in the private use section, and relies on "SF Pro"). ↩︎