Rotary encoder for Sonos volume control / light dimmer

Hi
I have been thinking about using a rotary encoder for Sonos volume control.
The idea would be that if it is turned, it would increase or decrease the volume a percentage per step. (an idea could be 100% per round, and it would then be 4,17% per step for a 24 step encoder)

The setup i’m thinking of:
Rotary encode -> arduino with ethernet shield -> MQTT -> Raspberry Pi (which runs OH)

Anybody with an better with a better suggestion?

Anyway i’m not sure how to make it, i only got the idea, so any help will be highly appreciated.

Thanks
Stefan

Sounds like it should work. You could eliminate the extra Arduino/Ethernet hardware and MQTT server by connecting the rotary encoder directly to the Pi.

Had not thought about that.
My biggest issue right now, is to make a rule that increases og decreases the volume.

Did you hook up a rotary encoder to any device yet?

No, I have not had the time to look at it yet. But all good suggestions are welcome

Hello there,

I recently started working on exactly what you’re describing here (Except it’s with an ESP8266) and I kinda got it to work, however I ran into some problems.

First I tried to use the MQTT eventbus of openhab. When I rotate the rotary encoder very slowly (1-2 clicks per second) everything works as it is supposed to. (One MQTT Message with the content “INCREASE” or “DECREASE” is dispatched every click). As soon as I rotate the encoder quicker, it seems like I’m overloading my MQTT broker (that has to handle the command from the ESP8266 and the updates from openHAB). So messages get delayed 4-5 seconds or don’t get delivered at all. So this solution is not working for me.

My second idea was to use the REST API and make HTTP-POST-Requests. Here I got a little bit better performance (4-5 clicks per second until I got funky results).

Does anybody know a better solution how to control items with a rotary encoder without hammering the REST-API with hundreds of HTML requests?:hammer:

It’s not likely you are overloading the MQTT broker unless you are running it on a very slow computer. It should be able to handle thousands of messages/sec without a problem. You’d typically overload the ESP8266 or the openHAB server before the MQTT broker.

What about throttling the message rate on the ESP8266 but sending the most recent message after an short idle period? So if you turn the knob quickly, the intermediate messages are dropped but the final value will still be sent.

One simple algorithm for doing the throttling is called a “token bucket”.

You could use this and save the most recently dropped message (clearing it when a message is successfully sent). A periodic timer could check the drop time for the most recent dropped message, if any, and send it after an idle period.

Hello @steve1,

thanks for the suggestion! I will definitely look into that and post the results later!

Alright, so I got it to work!

I do however believe, that a token bucket is not a good way to tackle this issue here and here’s why:
In oder to adjust the volume of a correctly I need instant feedback from the device. I don’t want to rotate the knob, wait a few seconds, just to notice that I totally overshot the desired volume (and potentially waking up the whole neighborhood in the process), wait for all the messages with “INCREASE” are processed, until I can turn the volume down again. The feedback loop is simply too long.

I did however come up with a similar Idea:

  • The ESP8266 requests the current value of the volume item from the OpenHAB-server via the REST
  • when the knob is rotated 1 is added or subtracted from “currentVolume” and “changed” is set to true
  • in each loop the function “transmit” is called, that just transmits “currentVolume” (also via REST), but only if the “delayTime” has passed (works pretty snappy, when it is set to 100 ms) and “changed” is true (so we don’t constantly transmit values even though nothing has changed). After transmitting the “changed” variable is set to false.
  • every few seconds the current value is updated from the OpenHAB-Server, in case somebody changed it elsewhere.
  • to connect to WiFi I used the library WiFiManager, which is a fantastic project!

This solution works quite nice for me.
I suppose one could also implement Play/Pause, Next, Previous Buttons like that…

I took the time to write a somewhat universal sketch (without any private libraries) that could be of use for some of you. If you find this useful, just consider pressing the like button…

#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager
#include <ESP8266HTTPClient.h>

///////////////////////////////User Settings ////////////////////////////////////////
String restAddress = "http://192.168.0.39:8080/rest/items/";
String volumeItemName = "SonosPLAY5_Volume";

///////////////////////////////Hardware Configuration/////////////////////////////////
//Configuration for KY-040 Rotary Encoder:
int pinA = 5;  // Connected to CLK on KY-040
int pinB = 4;  // Connected to DT on KY-040
int pinALast;
int aVal;
boolean bCW;

///////////////////////////////Other Variables////////////////////////////////////////
int delayTime = 100;    //Minimum delay time after one value has been sent to the openHAB server [ms]
int lastTransmit;       //used to store the millis() of the last time a value was sent

int pollingInterval = 2000; //how often is the actual volume of the Sonos fetched from the openHAB server [ms]
int lastVolumePoll;        //used to store the millis() of the last time a value was requested

int currentVolume;      //stores current Volume
boolean changed;        //Indicates if Volume has been changed since last send

HTTPClient http;  //Used to communicate with the REST interface of openHAB

//////////////////////End Variables////////////////////////
void transmit(String message) {  //Function to transmit Values to REST API
  if (changed) {               //Makes sure that messages are only transmitted if the value has been changed
    if (millis() > lastTransmit + delayTime) {  //checks if the delaytime has already been reached
      http.begin(restAddress + volumeItemName);
      http.addHeader("text/plain", "application/json");
      http.POST(message);         //Post value to openHAB server
      http.writeToStream(&Serial);  //If somethin goes wrong it will be written to Serial
      http.end();
      changed = false;          //Set the change Flag to false, so the value isn't constantly transmitted
      lastTransmit = millis();  //save the current time to variable so the delay can be calculated for the next transmit
    }
  }
}


void clockwise() {        //called, when encoder rotates clockwise...
  if (currentVolume < 100) { //don't raise volume above 100%
    currentVolume++;         //raise volume
    changed = true;          //set changeflag, so the transmit function knows that there's something to transmit
  }

}

void counterclockwise() {    //called, when encoder rotates clockwise...
  if (currentVolume > 0) {   //only accepts positive values
    currentVolume--;         //lower volume
    changed = true;          //set changeflag, so the transmit function knows that there's something to transmit
  }
}


int getCurrentVolume() {     //Function that gets current volume from openHAB server
  http.begin(restAddress + volumeItemName + "/state");    //Begin connection to the Server
  int httpCode = http.GET();          //Make GET-Request
  if (httpCode > 0) {                 //Check the returning code
    lastVolumePoll = millis();        //Remember when the Volume was fetched from the server
    if (httpCode = 200) {             //if HTTP-Response is OK
      String payload = http.getString();   //Get the request response payload
      http.end();                       //End HTTP-Connection
      return payload.toInt();           //Convert answer to integer and return that
    } else {         //If he HTTP-Response is not OK
      return 0;
    }
  }
  return 0; //HTTP-Code is 0
}


void setup() {
  Serial.begin(115200);
  pinMode (pinA, INPUT);
  pinMode (pinB, INPUT);
  pinALast = digitalRead(pinA);

  //WiFiManager
  //Local intialization. Once its business is done, there is no need to keep it around
  WiFiManager wifiManager;
  //fetches ssid and pass from eeprom and tries to connect
  //if it does not connect it starts an access point with the specified name
  //here  "AutoConnectAP"
  //and goes into a blocking loop awaiting configuration
  wifiManager.autoConnect("AutoConnectAP");
  Serial.println("connected...yeey :)");

  Serial.println("Getting current Volume...");
  currentVolume = getCurrentVolume(); //Get current Volume from item on openHAB server and save it
  Serial.print("Current Volume is: ");
  Serial.print(currentVolume);
  Serial.println("%");
}

void loop() {
  transmit(String(currentVolume));      //Call transmit function each loop

  //////////////////Reading the rotary encoder
  aVal = digitalRead(pinA);
  if (aVal != pinALast) { // Means the knob is rotating
    // if the knob is rotating, we need to determine direction
    // We do that by reading pin B.
    if (digitalRead(pinB) != aVal) {  // Means pin A Changed first - We're Rotating clockwise
      clockwise();
    } else {// Otherwise B changed first and we're moving counter clockwise
      counterclockwise();
    }
    pinALast = aVal;
  }
  //////////////////End reading the rotary encoder

  if (millis() > lastVolumePoll + pollingInterval) {      //check if it's time to request the Volume from the Server again..
    currentVolume = getCurrentVolume();                   //request Volume and set it to current Volume
    Serial.print("Volume has been Updated from the server to: ");
    Serial.print(currentVolume);
    Serial.println("%");
  }
}

Cheerio

2 Likes

Nice!

The token bucket approach should work fine if you are throttling incoming encoder events. The longest delay for processing the most recent throttled event and sending a message would be the loop delay (100 ms). It sounds like you were trying to throttle outgoing messages (not sure)? The periodic polling approach you used looks good too and is a bit simpler to implement.

Yes, I’m now throttling the outgoing messages.

Instead of sending an “INCREASE” at a time, I already add the number of steps I would like to increase to the current value of the dimmer item and then send the calculated value. This way I can do very many steps in a very short time, instead of being limited to 10 steps a second (100ms delay).

The periodic polling is probably not ideal yet though. Maybe I will implement MQTT in the future. That way I can reduce network traffic and request, since the device only gets a message, when the value changes via the MQTT event bus. I’d imagine however, that if there are quick updates of the value this could lead to unwanted results too, because if the device already sets a new value before the MQTT-message with the updated value gets in, things would get mixed up.

For now I think I’ll stick with this solution.

Thank You for Your support!!

1 Like