NodeMCU Dimmer

I thought I share this although it is still a bit rough on the edges.

I am using a KY-040 rotary encoder wired to a nodeMCU to have a physical dimmer.

The idea is to be able to turn the knob and my lights will be dimming accordingly.

I wired the KY-040 like this:

CLK   -> D1
DT    -> D2
SW    -> D3
+     -> 3.3V
GND   -> GND

This is the Arduino Sketch for the NodeMCU:

#include <ESP8266WiFi.h>

#include <PubSubClient.h>

// Update these with values suitable for your network.
const char* ssid = "SSID";
const char* password = "PASSWORD";
const char* mqtt_server = "BROKER_IP";
const char* topicEncoder = "dial/encoder";
const char* topicButton = "dial/button";
char charPos [5];

#define pinSW D3
#define pinA D1  // Connected to CLK on KY-040
#define pinB D2 // Connected to DT on KY-040
int encoderPosCount = 0; 
int pinALast;  
int aVal;
int Button;
int aButton;
boolean bCW;
String strTopic;
String strPayload;


WiFiClient espClient;
PubSubClient client(espClient);


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) {
  payload[length] = '\0';
  strTopic = String((char*)topic);
  if (strTopic == "dial/SetValue")
  {
    Serial.println("received");
    encoderPosCount = atoi((char*)payload);
    Serial.println (encoderPosCount);
  }
}

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


 void setup() { 
   pinMode (pinA,INPUT);
   pinMode (pinB,INPUT);
   pinMode (pinSW, INPUT);
   /* Read Pin A
   Whatever state it's in will reflect the last position   
   */
   pinALast = digitalRead(pinA);   
   Serial.begin (115200);
   setup_wifi();
   client.setServer(mqtt_server, 1883);
   client.setCallback(callback);
 } 

 void loop() { 
   if (!client.connected()) {
     reconnect();
   }
   

   if (!(digitalRead(pinSW))) {        // check if pushbutton is pressed
            client.publish(topicButton, "ON");
            while (!digitalRead(pinSW)) {}  // wait til switch is released
            delay(10);                      // debounce
            client.publish(topicButton, "OFF");        
   }
 
   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
       //client.publish(topicEncoder, "UP");
       encoderPosCount = encoderPosCount + 5;
       if (encoderPosCount >254) { encoderPosCount=254;}
       //delay(10);
     } else {// Otherwise B changed first and we're moving CCW
       //client.publish(topicEncoder, "DOWN");
       encoderPosCount = encoderPosCount - 5;
       if (encoderPosCount <0) { encoderPosCount=0; }
       //delay(10);
     }
     Serial.println(encoderPosCount);
     dtostrf(encoderPosCount,5,1,charPos) ;
     client.publish(topicEncoder, charPos);
     
   } 
   pinALast = aVal;
   client.loop();

 } 

The code needs some clean-up… but is working.

So whenever the knob is turned it sends the value to HASS via MQTT.
Same goes for the button press.

The value also can be set via MQTT from HASS - the idea here is that If I change the brightness for example via the Webinterface this will be reflected on the nodeMCU.

HASS configuration is a follows:

configuration.yaml:

sensor:
  - platform: mqtt
    name: encoder
    state_topic: "dial/encoder"
    qos: 0

  - platform: template
    sensors:
      turntable_brightness:
        value_template: '{{ states.light.turntable.attributes.brightness }}'

binary_sensor:
 - platform: mqtt
   name: button
   state_topic: "dial/button"
   qos: 0

Now I need the following automations to put things together:

- alias: 'Encoder changed'
  trigger:
    platform: state
    entity_id: sensor.encoder
  action:
    service: light.turn_on
    entity_id: light.turntable, light.floor_lamp, light.tv_lamp
    data_template:
      brightness: '{{ states.sensor.encoder.state | int }}'
      transition: 0.5

- alias: 'brightness changed'
  trigger:
    platform: template
    value_template: '{{(as_timestamp(now()) - as_timestamp(states.sensor.turntable_brightness.last_changed) > 20)  and (states.light.turntable.attributes.brightness | int != states.sensor.turntable_brightness | int)}}'
  action:
    service: mqtt.publish
    data:
      topic: 'dial/SetValue'
      payload_template: "{{ (states.light.turntable.attributes.brightness + states.light.floor_lamp.attributes.brightness + states.light.tv_lamp.attributes.brightness)/3 | int}}"

- alias: 'Button clicked'
  trigger:
    platform: mqtt
    topic: 'dial/button'
    payload: 'ON'
  action:
    service: light.toggle
    entity_id: light.turntable, light.floor_lamp, light.tv_lamp

And thats it.

The only problem so far is that the encoder is not reacting correctly if I turn it fast.
And there is considerable delay before applying the new brightness values. If anyone has an idea on how to streamline this it would be much appreciated.

5 Likes

I found this

which I suppose would be nice and create a virtual light for my dimmer but I still would have to map this virtual light to the existing hue lights.

Any one has an opinion?

Put the arduino code on github: https://github.com/snizzleorg/esp8266-Dimmer

I have the same kind of issue with a rotary encoder - if I rotate it too fast, I have some backwards changes… Did you find any solution to mitigate this issue?

I soldered 100nF capacitors between GND and the encoder pins.

That takes care of the bouncing.

2 Likes

It definitely makes sense, thanks.

It’s still not ideal since the automation stake a long time. and that makes the dimmer lag a lot. but at least If i turn it one way it changes the value only into one direction…

Well, I’m not using the same automation at all, mine is managing my AVR volume, and it’s almost instant. I just have an encoder issue for now - which should be solved soon, thanks to you :slight_smile:

Ok. How do you use it? is it directly connecting to a receiver or are you using HASS as intermediary?

The receiver is integrated into HA. I built a small remote thingy with a NodeMCU, an LCD screen and a rotary encoder. The rotary encoder manages the volume on the receiver, a click on it toggles play/pause on the currently playing source (MPD, Kodi, cast…), and the screen displays information on the currently playing media (title, artist, show title/episode number…).

As I wanted to integrate it with multiple sources of information, all communication is done with HA and not with the individual devices, all via MQTT. It’s working pretty great, I have some polishing to do, and I’ll eventually share all the details.

I’m loooking forward to that. Especially how you translate the encoder to the receiver. As this is where the lag in my case is happening.

I was thinking that maybe I should s ne the brightness directly to the hues instead of home assistant.

On the NodeMCU, I’m just listening for “up” or “down” events from the encoder (I don’t really care about the actual value, just the sign):

class EncoderListener : public EncoderManager::Listener {
    virtual void increment(int value) {
        pubSubClient.publish(MQTT_COMMAND_TOPIC, "volume_up", false);
        resetCommand();
    }

    virtual void decrement(int value) {
        pubSubClient.publish(MQTT_COMMAND_TOPIC, "volume_down", false);
        resetCommand();
    }

    virtual void click() {
        pubSubClient.publish(MQTT_COMMAND_TOPIC, "play_pause", false);
        resetCommand();
    }
};

On the Home Assistant front, I’m using AppDaemon, listening to an MQTT sensor bound to my command topic. I’m basically fetching the current volume from the AVR (via its HA state), incrementing/decrementing it, send I’m calling the media_player/volume_set service with the new value. I’ll keep you posted once everything is ready to share.

2 Likes

mark. I’m really interested in it!

@kernald Any news yet? did you get everything ready to share yet? I’m curious!

I didn’t have any time to clean it up, but here you go: https://git.enoent.fr/kernald/esp-media-remote

In addition to this, I have this AppDaemon application:

import appdaemon.appapi as appapi

class EspRemote(appapi.AppDaemon):
  def initialize(self):
      self.log("starting")
      self.listen_state(self.manage_event, self.args["command"])

  def manage_event(self, entity, attribute, old, new, kwargs):
      getattr(self, new)()

  def volume_up(self):
      self.set_volume(self.get_current_volume() + 0.01)

  def volume_down(self):
      self.set_volume(self.get_current_volume() - 0.01)

  def play_pause(self):
      for mp in self.split_device_list(self.args["media_players"]):
          if self.is_player_playing(mp):
              self.pause_player(mp)
              return

      for mp in self.split_device_list(self.args["media_players"]):
          if self.is_player_paused(mp):
              self.resume_player(mp)
              return

  def reset(self):
      pass

  def get_current_volume(self):
      volume = self.get_state(self.args["volume_control"], "volume_level")
      self.log("Got volume {}".format(volume))
      return volume

  def set_volume(self, volume):
      volume_truncated = "{0:.2f}".format(volume)
      self.log("Calling set volume with arg {}".format(volume_truncated))
      self.call_service("media_player/volume_set", entity_id = self.args["volume_control"], volume_level = volume_truncated)

  def is_player_playing(self, entityId):
      return self.get_state(entityId) == "playing"

  def is_player_paused(self, entityId):
      return self.get_state(entityId) == "paused"

  def pause_player(self, entityId):
      self.log("Pausing {}".format(entityId))
      self.call_service("media_player/media_pause", entity_id = entityId)

  def resume_player(self, entityId):
      self.log("Resuming {}".format(entityId))
      self.call_service("media_player/media_play", entity_id = entityId)

It toggles play/pause in different media players (list passed as media_players argument), and control volume of the entity volume_control.

I also have another application reporting media player metadata to MQTT:

import appdaemon.appapi as appapi

class Mpd2Mqtt(appapi.AppDaemon):
  def initialize(self):
      self.log("starting")

      self.listen_state(self.media_callback, self.args["mpd_entity"])
      self.listen_state(self.media_callback, self.args["kodi_entity"])

  def media_callback(self, entity, attribute, old, new, kwargs):
      if self.get_state(entity) == "playing":
        line0 = self.get_state(entity, "media_title")
        if self.get_state(entity, "media_artist"):
          line1 = self.get_state(entity, "media_artist")
        elif self.get_state(entity, "media_series_title"):
          line1 = self.get_state(entity, "media_series_title")
        else:
          line1 = ""
        if self.get_state(entity, "media_album_name"):
          line2 = self.get_state(entity, "media_album_name")
        elif self.get_state(entity, "media_season") and self.get_state(entity, "media_episode") and self.get_state(entity, "media_season") > -1 and self.get_state(entity, "media_episode") > -1:
          line2 = "S{:02d}E{:02d}".format(self.get_state(entity, "media_season"), self.get_state(entity, "media_episode"))
        else:
          line2 = ""

        self.call_service("mqtt/publish", payload = line0, topic = "media_player/esp/line0", qos = 0, retain = 0)
        self.call_service("mqtt/publish", payload = line1, topic = "media_player/esp/line1", qos = 0, retain = 0)
        self.call_service("mqtt/publish", payload = line2, topic = "media_player/esp/line2", qos = 0, retain = 0)
        self.call_service("mqtt/publish", payload = "", topic = "media_player/esp/line3", qos = 0, retain = 0)
2 Likes

cool. although the link does not get me anywhere I can see your code.
Anyways I was more interested how you got this working on the home-assistant end. Since my automations are slow and I think too complicated. So your app-daemon approach is probably much more elegant.

Thanks

Sorry about that, I missed a setting in GitLab. You should be able to see the code now, if you’re curious :slight_smile:

Would be awesome for a thermostat project (with oled screen for displaying set and current temperature), but I’m too noob to do it :(.