Mqtt dimmer with rotary encoder

I was missing the possibility to use an old style twist type dimmer with my home automation so i tried to use a nodeMCU board with MQTT to send dimmer values to openhab using a KY-040 rotary encoder. I can use the same encoder to dim several lamps by pushing the button to change modes and i used LEDs to indicate witch lamp is being controlled.

It turned out quite usable, downsides is that is doesn’t read values from openhab so if i use sitemaps to dim first then use the encoder it will start at the value the encoder was last used, also it doesn’t dim very smooth. But i can live with that.

Feel free to comment my code but be nice. I am a complete rookie in coding. Most of it is shamelessly copied from various examples I found around the internet.

NodeMCU Code

#include <Encoder.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

Encoder myEnc(D1, D2);

// Update these with values suitable for your network.
const char* ssid = "0ff984";
const char* password = "************";
const char* mqtt_server = "192.168.0.18";
char msgDim[50];
char msgMode[50];
int counter = 1;
int oldCounter = 0;
long oldPosition  = -999;
boolean isButtonPressed = false;
long lastUpdateMillis = 0;


//Buttons
int buttonState;
int LED1 = D6;
int LED2 = D7;
int LED3 = D8;


WiFiClient espClient;
PubSubClient client(espClient);

void handleKey() {
  isButtonPressed = true;  
}

void setup() {

  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);

  Serial.begin(115200);
  Serial.println("Basic Encoder Test:");
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  
   pinMode(D3, INPUT_PULLUP);
   attachInterrupt(D3, handleKey, RISING);
}

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // Switch on the LED if an 1 was received as first character
  if ((char)payload[0] == '1') {
    digitalWrite(BUILTIN_LED, LOW);   // Turn the LED on (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is acive low on the ESP-01)
  } else {
    digitalWrite(BUILTIN_LED, HIGH);  // Turn the LED off by making the voltage HIGH
  }

}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("inTopic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}





void loop() {

  if (!client.connected()) {
    reconnect();
    }
  client.loop();
  
  long newPosition = myEnc.read();
  if (newPosition < 0){
    myEnc.write(0);
  }
  if (newPosition > 100){
    myEnc.write(100);
  }
  
  if (newPosition != oldPosition) {
    oldPosition = newPosition;
    //Serial.println(newPosition);
    snprintf (msgDim, 75, "%ld", newPosition);
    client.publish("Lights/Dimmer", msgDim);
  }
  // software debounce
  if (isButtonPressed && millis() - lastUpdateMillis > 50) {
    isButtonPressed = false;
    lastUpdateMillis = millis();
    // Up counter on button press
    counter = counter + 1;
   //Reset count if over max mode number
   if(counter == 4) {
      counter = 1;
   }
  }
 //Change mode

 if(counter == 1) {
   digitalWrite(LED1, HIGH);
   digitalWrite(LED2, LOW);
   digitalWrite(LED3, LOW);  
 }
 else if(counter == 2)  {
   digitalWrite(LED1, LOW);
   digitalWrite(LED2, HIGH);
   digitalWrite(LED3, LOW);
 }
 else if(counter == 3)  {
   digitalWrite(LED1, LOW);
   digitalWrite(LED2, LOW);
   digitalWrite(LED3, HIGH);
 }
if (counter != oldCounter) {
    oldCounter = counter;
    snprintf (msgMode, 75, "%ld", counter);
    client.publish("Lights/Dimmer/Mode", msgMode);
}
 //Serial.println(counter);
}

Items

/*MQTT Dimmers*/
Number Rotary_Encoder   "Rotary Encoder [%.1f]"    {mqtt="<[broker:Lights/Dimmer:state:default]",autoupdate="false"}
Number Rotary_Encoder_Mode   "Rotary Encoder Mode[%.1f]"    {mqtt="<[broker:Lights/Dimmer/Mode:state:default]",autoupdate="false"}

Openhab Rules

rule "Rotary encoder"
when
    Item Rotary_Encoder changed
then
    var LightIntensity1 = Rotary_Encoder.state as Number
	if (Rotary_Encoder_Mode.state==1){
		sendCommand(Dimmer_Downlight_Stue, LightIntensity1 as Number)
	}
	if (Rotary_Encoder_Mode.state==2){
		sendCommand(Dimmer_Lampetter, LightIntensity1 as Number)
	}

end

1 Like

Cool, show us a picture of your awesome modern retro hardware :slight_smile:

Hehe that step is yet to come, plan is to mount it in a blank faceplate but i don’t know what to use for power. in the meantime its all connected in a protoboard.

I’ve not enough experience with Arduino code yet to comment on the NodeMCU code. ButI do have a suggestion for the OH Rules.

  • Where possible always use the Item method instead of the actions for sendCommand and postUpdate: https://docs.openhab.org/configuration/rules-dsl.html#sendcommand-method-vs-action

  • Since the Encoder Mode can only have one of two states you can use a Switch or a Contact Item instead of a Number and use a transform in the MQTT config to convert the 1/2 to ON/OFF or OPEN/CLOSED.

  • I try to encourage structuring Rules as follows: First calculate what needs to be done and then do it. This usually results in shorter rules but the real benefit is that should you need to do more work or change what needs to be done you only have to change it in one place.

rule "Rotary encoder"
when
    Item Rotary_Encoder changed
then
    // calculate what needs to be done
    if(Rotary_Encoder.state == NULL) return; // fail fast

    val lightIntensity = RotaryEncoder.state as Number
    val d = if(Rotary_Encoder.state == 1) Dimmer_Downlight_Stue else Dimmer_Lampetter // assuming only 1 and 2 are valid states

    // do it
    d.sendCommand(lightIntensity)
end

With the above, if you wanted to add some logging or some additional checking or calculations before sending the command to dim the light, now you only have to do it in one place instead of in each clause of an if/else.

Thanks for posting! When you figure out how to power it and install it I bet the community would love to see the hardware details as well.

Thanks for the reply:-) As you maybe can see in the Arduino code the encoder mode can have more states( 3 so far but can be more), so I could not figure out how to do it without several IF statements. Can your example be adapted to that?

Yes

rule "Rotary encoder"
when
    Item Rotary_Encoder changed
then
    // calculate what needs to be done
    if(Rotary_Encoder.state == NULL) return; // fail fast

    val lightIntensity = RotaryEncoder.state as Number
    val d = null
    switch Rotary_Encoder_Mode.state as Number {
        case 1: d = Dimmer_Downlight_Stue
        case 2: d = Dimmer_Lampetter
        case 3: d = ???
    }

    // do it
    d.sendCommand(lightIntensity)
end

Thank you very much :slight_smile: I will try it out tonight.