Rebooting all of your Sonos devices

A Sonos Play One device under a shelf

[UPDATE 12/07/2018: Sonos has now discontinued the reboot functionality through their UPnP API, so this will no longer work.]

I’m an avid Sonos user. I’ve used their devices for many years now and love the platform, sans one feature — the ability to reboot all of your Sonos zones on your network at one time.

Fortunately, Sonos provides a bunch of advanced capabilities through a web interface. One of the capabilities buried in these hidden pages is the ability to reboot a single zone. I decided to use the interface to automate the reboot of all of the zones on my network. (The full code from this post can be found on GitHub)

The first thing I needed to do was enumerate the list of zones on my network which can be done easily using the /support/review interface. The output of this interface is XML and can easily be traversed with common libraries.

zpnetworkinfo = ElementTree.fromstring(zoneget.content)
for zpsupportinfo in zpnetworkinfo.iter('ZPSupportInfo'):
zpinfo = zpsupportinfo.find('ZPInfo')
zonename = zpinfo.find('ZoneName').text
ipaddress = zpinfo.find('IPAddress').text

Now that I’ve grabbed a full list of IP addresses for my Sonos zones, my hope was I could simply iterate through the list and issue the reboot command. Unfortunately, I was wrong. Since Sonos is a mesh network the order in which you reboot the zones does matter and can cause unexpected communications failures when issuing the reboot command. To simplify this I broke my zones down into three different categories:

  1. Zones which are only connected by ethernet — these zones can be rebooted right away.
  2. Zones which are only connected by a wireless signal — the zones with the largest number of peers will be rebooted last to ensure there is enough connectivity in the network for the reboot command to be received successfully by all zones.
  3. Zones which are connected both by WiFi and Ethernet — These will be last. One sticking point I discovered here is I have a Playbar which is joined to Sonosnet wirelessly and also has a Connect:Amp peering with it over ethernet for 5.1 audio. This configuration causes my Playbar to erroneously fall into this last bucket (I could remedy this by building a tree of my Sonos network to use when sending the reboot command).

We can identify which category our zones fall in by parsing the output of the /usr/sbin/brctl showstp br0 command found in our XML. I chose to use regular expressions to parse this output, and did some basic arithmetic to put the zones into their respective category.

commands = zpsupportinfo.iter("Command")
for command in commands:
if command.attrib["cmdline"].startswith('/usr/sbin/brctl showstp'):
   # Count active ethernet interfaces
pattern = re.compile(r'eth\d.*?state.*?$', re.DOTALL | re.MULTILINE)
matches = pattern.finditer(command.text)
if matches is not None:
for match in matches:
result = match.group()
pattern = re.compile(r'forwarding', re.DOTALL)
match = pattern.search(result)
if match is not None: ethernetcount += 1
  # Count active wireless interfaces
pattern = re.compile(r'ath\d.*?state.*?$', re.DOTALL | re.MULTILINE)
matches = pattern.finditer(command.text)
if matches is not None:
for match in matches:
result = match.group()
pattern = re.compile(r'forwarding', re.DOTALL)
match = pattern.search(result)
if match is not None: wirelesscount += 1
#Calculate reboot order weight
rebootorder = 0
if wirelesscount != 0:
rebootorder = 1000*ethernetcount+wirelesscount
zone = [zonename, ipaddress, rebootorder]
zones.append(zone)

Now that we have a list of zones and reboot order the next step is to actually make the web call to reboot the zone. Unlike most of the other hidden pages in Sonos, the reboot page utilizes a web form, and has a token to check for cross-site reference forgeries (CSRF). We’ll need to actually make two web requests here, one to get the CSRF token, and a second to submit the reboot command.

def rebootzone(ipAddress):
session = RequestsSessions.session()
session.keep_alive = False
session.mount('http://', RequestsAdapters.HTTPAdapter(max_retries=5))
rebooturl = "http://" + ipAddress + ":1400/reboot"
rebootget = session.get(rebooturl, timeout=5)
rebootresponse = ElementTree.fromstring(rebootget.text)
for body in rebootresponse:
csrftoken = body.find('form').find('input').attrib["value"]
session.post(rebooturl, { "csrfToken" : csrftoken }, timeout=5)
return

Your Sonos zones should now be rebooting.

If you’d like to re-use this, please check out the complete code sample on GitHub which includes a lot of the boiler plate code left out in this post.