Introduction: SONOS Remote Control

Sonos speakers are great sounding and highly practical since you can control them from your phone, tablet or computer. I use the one installed in my bedroom to wake me up in the morning to my favorite tune. The speaker is installed at the other end of the room so if I want to stop the alarm, I must either walk to the speaker or launch the Sonos application from my mobile phone which is not always nearby since I don’t typically sleep with my phone. In order to wake up more gently, I decided to build this Sonos remote control. It allows to stop the alarm, play/pause the currently playing tune, skip to the next one and raise/lower the volume.

It is based on a ESP8266 combined with a cheap ATTINY85 that allows easier and better power management and will give you over a year of operation on a small 500 mAh Li-Po battery.

IMPORTANT: In order to get this remote to work, you’ll need a device able to run node.js. This could be a desktop computer or a Raspberry Pi. On top of it, you will need the excellent Sonos HTTP API by jishi which is based on node.js. I installed it on my desktop and configured it as a service. More on this later.

Step 1: Material

Here is the list of material you will need

Step 2: Installing the SONOS HTTP API Software

I recommend reading this article about the HTTP API’s installation procedure.

https://bartsimons.me/controlling-sonos-devices-wi...

Once this is done and you’ll be able to send commands to your in-house web server in the form: http://localhost:5005/Room/playpause

Make sure your host based firewall allows this traffic by testing the connection from another device on the same network. Once this works, we can proceed and configure the web server as a service.

Configuring the HTTP API as a service

First, we need to download and install the nssm software found here: https://nssm.cc/download

Open an Administrative command prompt and type the following:

c:\> NSSM install SONOS

In the dialog box, fill up with the parameters as shown in the images above. Make sure you specify the paths and directories you used when installing Node.js and the SONOS HTTP API.

Once configured, you can hit the "Install Service" button. This way, you’ll be sure the Sonos API HTTP server will always automatically start when you reboot your machine.

Step 3: NETWORK Configuration (optional Step)

In order to speed up the connection time required by the ESP01 to connect to the network and issue the command to the SONOS HTTP API server, I recommend that you bypass the DNS and DHCP requests and use the direct IP addresses instead. This is done in your home router where you need to specify the network MAC address of the machine hosting the HTTP server as well as the MAC of the ESP01. In my NetGear router, this is done through the LAN Setup menu. In my case, the DESKTOP device runs the SONOS HTTP API webserver and the SonosRem is the Sonos remote we’re about to build.

Check your own router user’s manual on how to do this on your specific router.

Step 4: Building the Circuit

I initially built this circuit without the ATTINY85. It worked fine but I went through batteries way to fast, even though the ESP01 was almost always in deep sleep mode with the LED unsoldered. This might have been caused by some object being left on the remote who kept the button pushed indefinitely. I'll never know exactly but I still wanted to handle this situation.

Using the ATTINY85 as proved to be a wise choice for this. The ATTINY code is using less than 2uA while in sleep mode. I use hardware interrupts on the buttons to wake it up. Once triggered, the ATTINY finds out which button was pressed, single or double-clicked. It then switches the ESP01 on through the 2N3904 transistor, wait for it to come online and sends the command associated with the button pressed.

The first command sent will take between 3 to 10 seconds to perform since the ESP01 has to boot, connect to Wifi and send the command to the web server. The ESP01 stays awake for another 20 seconds after the last command in order to avoid the repeat of the connection time.

Step 5: The Code

Here is the Attiny and ESP01 code. You will have to install Nick Gammon's SendOnlySoftwareSerial library from here to be able to compile the Attiny code.

I had to resort to use this library because of incompatibilities between the different SoftwareSerial libraries I tried and the Interrupt handling libraries of the Attiny. Because of this, the ESP01 signals it is done with commands requested by the Attiny by raising its GPIO pin to HIGH for a few ms.

Sonos_Remote_Control_Attiny.ino

#include<SendOnlySoftwareSerial.h>
#include<avr/sleep.h>
#include<avr/power.h>// Power management
#include<avr/wdt.h>
#ifndef cbi
#definecbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#definesbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
// ATMEL ATTINY 25/45/85 / ARDUINO
//
// +-\/-+
// Ain0 (D5) PB5 1| |8 Vcc
// Ain3 (D3) PB3 2| |7 PB2 (D2) Ain1
// Ain2 (D4) PB4 3| |6 PB1 (D1) pwm1
// GND 4| |5 PB0 (D0) pwm0
// +----+
// ****** PINS USAGE ************
// unused // pin #1
#definePLAY_PAUSE3// pin #2
#definePOWER_ESP4// pin #3
// GND // pin #4
// Serial TX to ESP // pin #5
#defineESP_FLAG1// pin #6
#defineVOLUME_UP2// pin #7
// double click options, +2 the value of single click button
#defineNEXT_SONG13
#defineVOLUME_DOWN12
#defineNOTHING0
#defineONtrue
#defineOFFfalse
#defineBUTTON_TIMEOUT500
#defineESP_WAKE_TIME20000
SendOnlySoftwareSerial esp(0); // TX
int button;
bool esp_on = false;
bool goToSleep = true;
unsignedlong powerOnTime, delta;
voidsystem_sleep()
{
cbi(ADCSRA,ADEN);
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode();
sleep_disable();
sbi(ADCSRA,ADEN);
}
ISR(PCINT0_vect)
{
// This is called only when the PIR interrupt occurs
}
voidsendCommand(int c)
{
int to=0;
esp.print(c);
esp.flush();
while ((to++< 5000) && (digitalRead(ESP_FLAG) == LOW))
{
delay(1);
}
}
voidswitchEsp(bool desiredState)
{
if (desiredState == ON)
{
powerOnTime = millis();
if (esp_on)
return;
digitalWrite(POWER_ESP, HIGH);
delay(2000);
esp_on = true;
while (digitalRead(ESP_FLAG) == LOW)
{
delay(1);
}
powerOnTime = millis();
}
else// OFF
{
if (!esp_on)
return;
digitalWrite(POWER_ESP, LOW);
esp_on = false;
}
}
voidsetup()
{
pinMode(POWER_ESP, OUTPUT);
pinMode(ESP_FLAG, INPUT);
pinMode(PLAY_PAUSE, INPUT_PULLUP);
pinMode(VOLUME_UP, INPUT_PULLUP);
PCMSK |= 0b00001100; //_BV(PCINT3); // Use PB2 & PB3 as interrupt pin for PIRs signals
GIFR |= _BV(PCIF); // clear any outstanding interrupts
GIMSK |= _BV(PCIE); // Enable Pin Change Interrupts
esp.begin(9600);
}
voidloop()
{
if (goToSleep)
{
system_sleep();
}
else
{ // Check if we need to switch off the ESP or let it run
delta = 0;
while ((delta < ESP_WAKE_TIME) && (digitalRead(PLAY_PAUSE)==HIGH) && (digitalRead(VOLUME_UP)==HIGH))
{
delta = millis() - powerOnTime;
delay(1);
}
if (delta >= ESP_WAKE_TIME || delta==0)
{
switchEsp(OFF);
goToSleep = true;
return;
}
}
goToSleep = false;
button = NOTHING;
if (digitalRead(PLAY_PAUSE)==LOW)
button = PLAY_PAUSE;
if (digitalRead(VOLUME_UP) == LOW)
button = VOLUME_UP;
int cntr=0;
while ((digitalRead(button)==LOW) && (cntr++ < BUTTON_TIMEOUT))
delay(1);
cntr = 0;
while ((digitalRead(button)==HIGH) && (cntr++ < BUTTON_TIMEOUT))
delay(1);
if (cntr < BUTTON_TIMEOUT) //
{
cntr = 0;
while ((digitalRead(button) == LOW) && (cntr++ < BUTTON_TIMEOUT))
delay(1);
if (cntr < BUTTON_TIMEOUT)
button+=2; // increase the button value for double-click
else
{ // The button is still pressed, not normal. Go back to sleep and wait for another event
goToSleep = true;
switchEsp(OFF);
return;
}
}
switchEsp(ON);
if (button == PLAY_PAUSE)
sendCommand(1);
if (button == NEXT_SONG)
sendCommand(2);
if (button == VOLUME_UP)
sendCommand(3);
if (button == VOLUME_DOWN)
sendCommand(4);
}

SONOS_Remote_Control_ESP.ino

/**
* BasicHTTPClient.ino
*
* Created on: 24.05.2015
*
*/
#include<Arduino.h>
extern"C"{
#include"user_interface.h"
}
#include<ESP8266WiFi.h>
#include<WiFiClient.h>
#include<ESP8266HTTPClient.h>
IPAddress staticIP(192, 168, 1, 22); //ESP static ip
IPAddress gateway(192, 168, 1, 1); //IP Address of your WiFi Router (Gateway)
IPAddress subnet(255, 255, 255, 0); //Subnet mask
IPAddress dns(192, 168, 1, 1); //DNS
HTTPClient http;
voidsignalDone()
{
digitalWrite(2, HIGH);
delay(10);
digitalWrite(2, LOW);
}
voidsetup()
{
pinMode(2, OUTPUT);
digitalWrite(2, LOW);
Serial.begin(9600);
WiFi.config(staticIP, subnet, gateway,dns);
WiFi.mode(WIFI_STA);
WiFi.begin("ACCESS_POINT_NAME", "PASSWORD");
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
}
delay(500);
// Signal the Attiny we're done
signalDone();
}
voidloop() {
char c;
while (Serial.available())
{
c = Serial.read();
if ( c == '1')
http.begin("http://192.168.1.9:5005/Room/playpause"); //HTTP
if ( c == '2')
http.begin("http://192.168.1.9:5005/Room/next"); //HTTP
if ( c == '3')
http.begin("http://192.168.1.9:5005/Room/volume/+5"); //HTTP
if ( c == '4')
http.begin("http://192.168.1.9:5005/Room/volume/-5"); //HTTP
http.GET();
String payload = http.getString();
if (payload!="{\"status\":\"success\"}")
{
http.GET();
}
http.end();
signalDone();
}
}

Step 6: Building the Device

I made a PCB with the components soldered and 3D printed an enclosure for the device. Please note that I used breakaway headers for the ESP01 and machine pin breakaway headers for the ATTINY so the chips can easily be removed and updated if needed in the future.

I also 3D printed a device holder so the remote can serve as an easy to reach “Stop the alarm” button in the morning. I position the remote upside down on it. There is step in the bottom of the holder where the top button sits. Pressing on the back of the device sends a play/pause command to the SONOS speaker.

I hope you like it. Let me know if you have questions or comments, I try to answer every single one of them.

Step 7: Using the Device

Top button (single click) : PlayPause

Top button (double click): PlayNext song

Bottom button (single click) : VolumeUp

Bottom button (double click) : VolumeDown