The phone rings, and I grab for the nearest Sonos controller, power that up, wait a few seconds until the wireless connection is there and hit the pause button. If I can't find the iPod controller, I may have to start the Sonos desktop controller application to then hit pause. While I'm trying to cut off the music, the phone continues ringing. Not nice: I don't like answering the phone with music playing in the background.

Sonos zone players have a mute button, but I'm not always near a Sonos player, and that wouldn't help anyway: the mute control on a zone player mutes just that one station -- music keeps on blaring out of the other zones even when they are linked together, as is usually the case chez Mens. (I can mute all zone players in the house by holding the mute button down for 3 seconds, but that takes too long.)

A discussion on pause vs. mute over at Volker's site got me thinking that it ought to be possible to "talk" to a Sonos zone player and request that it pause. These devices speak UPNP, so a bit of HTTP should do the trick. At least one person has been there and done that, and the required SOAP packets are neatly laid out.

And so the idea of a Sonos Pause Switch was born: an intelligent yet small device that fires off a HTTP request to a Sonos zone player; an ideal task for an Arduino. The zone player I contact determines which of the players are paused -- all zones linked to it pause or play simultaneously.

The device proper is quickly implemented: an Arduino, one Ethernet shield, a resistor, a push button, and a LED.

The code is easy enough as well: the push button acts as a switch toggling the state to either of PLAY or PAUSE. Depending on the state when the button is pushed, I send out a SOAP request to the zone player to request it to PLAY or PAUSE respectively.

#include <Client.h>
#include <Ethernet.h>
 
/* Sonos Pause Button
 * by Jan-Piet Mens, March 2010
 * based on packets crafted by http://www.ip-symcon.de/forum/f39/einzelne-sonos-player-ansteuern-3541/
  */
 
static uint8_t mac[6] = { 0xBE, 0xEF, 0xEE, 0x00, 0x20, 0x09 };
static uint8_t ip[4] = { 192, 168, 1, 209 };
byte sonosip[] = { 192, 168, 1, 132 };
 
int debug = 0;
 
#define SONOS_PAUSE "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:Pause xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\"><InstanceID>0</InstanceID></u:Pause></s:Body></s:Envelope>"
#define SONOS_PLAY  "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:Play xmlns:u=\"urn:schemas-upnp-org:service:AVTransport:1\"><InstanceID>0</InstanceID><Speed>1</Speed></u:Play></s:Body></s:Envelope>"
 
#define PLAY 1
#define PAUSE 0
 
// Debounce code from http://arduino.cc/en/Tutorial/Debounce
 
const int buttonPin = 2;     // the number of the pushbutton pin
const int ledPin =  8;      // the number of the LED pin
 
int state = LOW;      // the current state of the output pin
int reading;           // the current reading from the input pin
int previous = LOW;    // the previous reading from the input pin
 
long time = 0;         // the last time the output pin was toggled
long debounce = 200;   // the debounce time, increase if the output flickers
 
Client client(sonosip, 1400);
 
void setup()
{
  delay(3000);
  Ethernet.begin(mac, ip);
  pinMode(buttonPin, INPUT);
  pinMode(ledPin, OUTPUT);
 
  if (debug) {
    Serial.begin(9600);
  }
}
 
void loop()
{
 
  http://www.arduino.cc/en/Tutorial/Switch
 
  reading = digitalRead(buttonPin);
 
  // if the input just went from LOW and HIGH and we've waited long enough
  // to ignore any noise on the circuit, toggle the output pin and remember
  // the time
  if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    if (state == HIGH) {
      state = LOW;
      if (debug) {
        Serial.println("play");
      }
      sonos(PLAY);
    }
    else {
      state = HIGH;
      if (debug) {
        Serial.println("pause");
      }
      sonos(PAUSE);
    }
 
    time = millis();
  }
 
  digitalWrite(ledPin, state);
 
  previous = reading;
}
 
void out(const char *s)
{
  client.println(s);
  if (debug) {
    Serial.println(s);
  }
}
 
void sonos(int cmd)
{
  char buf[512];
 
  if (client.connect()) {
    if (debug) {
      Serial.println("connected");
    }
 
    out("POST /MediaRenderer/AVTransport/Control HTTP/1.1");
    out("Connection: close");
    sprintf(buf, "Host: %d.%d.%d.%d:1400", sonosip[0], sonosip[1], sonosip[2], sonosip[3]);
    out(buf);
 
    sprintf(buf, "Content-Length: %d", (cmd == PLAY) ? strlen(SONOS_PLAY) : strlen(SONOS_PAUSE));
    out(buf);
 
    out("Content-Type: text/xml; charset=\"utf-8\"");
 
    sprintf(buf, "Soapaction: \"urn:schemas-upnp-org:service:AVTransport:1#%s\"", (cmd == PLAY) ? "Play" : "Pause");
    out(buf);
 
    out("");
    strcpy(buf, (cmd == PLAY) ? SONOS_PLAY : SONOS_PAUSE);
    out(buf);
 
    while (client.available()) {
      char c = client.read();
      if (debug) {
        Serial.print(c);
      }
    }
 
  } else {
    if (debug) {
      Serial.println("connection failed");
    }
  }
  client.stop();
}

So far the "device" has been working very well for me. Just once, I was able to "confuse" an S5 so much, that it continuously issued a UPNP Error code 701 when trying to pause it -- a factory reset of the zone player cured that, and it hasn't happened since. Hmm.

This is a proof of concept, and it works well. The following (wobbly) video shows the result: when the LED lights up, the Sonos system is paused. (Turn the volume up to "hear" the result.) There are, however, lots of ways to improve on this, for example

  • The device must be smaller; an Arduino Nano would be a good starting point, but then there's the issue with the Ethernet connection -- there's no Ethernet shield for the Nano.
  • As implemented above, the device needs an Ethernet cable attached to it. With a bit more intelligence, it could be made wireless -- with XBee or BlueTooth for example.
  • Ideally, the whole device is no larger than a car key; something like a wireless (WLAN) device that invokes a pre-defined CGI for on and for off.
  • An "intelligent" VOIP solution (Asterisk or FreeSwitch) could issue the UPNP requests itself upon receiving an incoming call, making the device obsolete.

Of course I still need a sexy case for the Sonos Pause Button -- that is always the hardest. :-)

Flattr this
Arduino, sonos, and hack :: 16 Mar 2010 :: e-mail

Comments

blog comments powered by Disqus