HomeBridge – MQTT Blinds

After several questions about automating blinds, I found the solution. In this blog I show you how to automate your blinds.

Homebridge

You need to setup your Apple Homebridge based on MQTT first to connect this setup with Apple HomeKit / Siri. For whom did not read my previous blog about setting up your Apple Homebridge based on MQTT you can find it here.

The hardware

NodeMcu v3 Lua ESP-12E WIFI Development Board
28BYJ-48 Stepper Motor
ULN2003 Stepper Motor Driver board
Breadboard
5 – 9V poweradapter

When I have collected al the necessary parts, you can setup the test environment like show in the image below.

As you can see I used a NodeMcu v3 Lua ESP-12E WIFI Development Board in this setup to simplify the setup. Connect the ULN2003 Stepper Motor Driver board to to your NodeMcu v3 Lua ESP-12E WIFI Development Board as following:

D1 = IN1, D3 = IN2, D2 = IN3, D4 = IN4

Connect 5V directly to Vin on NodeMCU board  and to the ULN2003 board. 5V may simply be not enough to be able to control the blinds, even with the higher gear ratio, The ULN2003 can go up to 12V if nessecarry. I’m using the 5V version of the stepper motor and it has worked well for me so far – there’s an 12V version though that should be able to give a higher torque than the 5V. My test roller blind is just 100cm wide and if you have a really wide roller blind I would probably go for the 12V version of the stepper motor.



Software

The Software is a WebSocket based version, So there is no need of an MQTT server but MQTT is supported as well – you can control it with WebSockets and with MQTT messages. How does it work you ask, well a tiny webserver is setup on the esp8266 that will serve one page to the client. Upon powering on the first time WIFI credentials, a hostname and – optional – MQTT server details is to be configured. But more about this later.

The Code
#include <Stepper_28BYJ_48.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <PubSubClient.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
#include <WiFiManager.h>
#include <ArduinoJson.h>
#include "FS.h"
#include <WiFiClient.h>

#include <WebSocketsServer.h>

// Version number for checking if there are new code releases and notifying the user
String version = "1.2.0";

//Configure Default Settings for AP logon
String APid = "AutoConnectAP";
String APpw = "StudioPieters";

//Fixed settings for WIFI
WiFiClient espClient;
PubSubClient psclient(espClient);   //MQTT client
String mqttclientid;         //Generated MQTT client id
char mqtt_server[40];             //WIFI config: MQTT server config (optional)
char mqtt_port[6] = "1883";       //WIFI config: MQTT port config (optional)
String outputTopic;               //MQTT topic for sending messages
String inputTopic;                //MQTT topic for listening
boolean mqttActive = true;

char config_name[40];             //WIFI config: Bonjour name of device

String action;                      //Action manual/auto
int path = 0;                       //Direction of blind (1 = down, 0 = stop, -1 = up)
int setPos = 0;                     //The set position 0-100% by the client
long currentPosition = 0;           //Current position of the blind
long maxPosition = 2000000;         //Max position of the blind. Initial value
boolean loadDataSuccess = false;
boolean saveItNow = false;          //If true will store positions to SPIFFS
bool shouldSaveConfig = false;      //Used for WIFI Manager callback to save parameters
boolean initLoop = true;            //To enable actions first time the loop is run

Stepper_28BYJ_48 small_stepper(D1, D3, D2, D4); //Initiate stepper driver

WiFiServer server(80);              // TCP server at port 80 will respond to HTTP requests
WebSocketsServer webSocket = WebSocketsServer(81);  // WebSockets will respond on port 81


/****************************************************************************************
   Loading configuration that has been saved on SPIFFS.
   Returns false if not successful
*/
bool loadConfig() {
  File configFile = SPIFFS.open("/config.json", "r");
  if (!configFile) {
    Serial.println("Failed to open config file");
    return false;
  }

  size_t size = configFile.size();
  if (size > 1024) {
    Serial.println("Config file size is too large");
    return false;
  }

  // Allocate a buffer to store contents of the file.
  std::unique_ptr<char[]> buf(new char[size]);

  // We don't use String here because ArduinoJson library requires the input
  // buffer to be mutable. If you don't use ArduinoJson, you may as well
  // use configFile.readString instead.
  configFile.readBytes(buf.get(), size);

  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& json = jsonBuffer.parseObject(buf.get());

  if (!json.success()) {
    Serial.println("Failed to parse config file");
    return false;
  }
  json.printTo(Serial);
  Serial.println();

  //Store variables locally
  currentPosition = long(json["currentPosition"]);
  maxPosition = long(json["maxPosition"]);
  strcpy(config_name, json["config_name"]);
  strcpy(mqtt_server, json["mqtt_server"]);
  strcpy(mqtt_port, json["mqtt_port"]);

  return true;
}

/**
   Save configuration data to a JSON file
   on SPIFFS
*/
bool saveConfig() {
  StaticJsonBuffer<200> jsonBuffer;
  JsonObject& json = jsonBuffer.createObject();
  json["currentPosition"] = currentPosition;
  json["maxPosition"] = maxPosition;
  json["config_name"] = config_name;
  json["mqtt_server"] = mqtt_server;
  json["mqtt_port"] = mqtt_port;

  File configFile = SPIFFS.open("/config.json", "w");
  if (!configFile) {
    Serial.println("Failed to open config file for writing");
    return false;
  }

  json.printTo(configFile);

  Serial.println("Saved JSON to SPIFFS");
  json.printTo(Serial);
  Serial.println();
  return true;
}
/****************************************************************************************
*/

/*
   Connect to MQTT server and publish a message on the bus.
   Finally, close down the connection and radio
*/
void sendmsg(String topic, String payload) {
  if (!mqttActive)
    return;

  Serial.println("Trying to send msg..." + topic + ":" + payload);
  //Send status to MQTT bus if connected
  if (psclient.connected()) {
    psclient.publish(topic.c_str(), payload.c_str());
  } else {
    Serial.println("PubSub client is not connected...");
  }
}
/*
   Connect the MQTT client to the
   MQTT server
*/
void reconnect() {
  if (!mqttActive)
    return;

  // Loop until we're reconnected
  while (!psclient.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (psclient.connect(mqttclientid.c_str())) {
      Serial.println("connected");

      //Send register MQTT message with JSON of chipid and ip-address
      sendmsg("/raw/esp8266/register", "{ \"id\": \"" + String(ESP.getChipId()) + "\", \"ip\":\"" + WiFi.localIP().toString() + "\", \"type\":\"roller blind\", \"name\":\"" + config_name + "\"}");

      //Setup subscription
      psclient.subscribe(inputTopic.c_str());

    } else {
      Serial.print("failed, rc=");
      Serial.print(psclient.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      ESP.wdtFeed();
      delay(5000);
    }
  }
}
/*
   Common function to get a topic based on the chipid. Useful if flashing
   more than one device
*/
String getMqttTopic(String type) {
  return "/raw/esp8266/" + String(ESP.getChipId()) + "/" + type;
}

/****************************************************************************************
*/
void processMsg(String res, uint8_t clientnum) {
  /*
     Check if calibration is running and if stop is received. Store the location
  */
  if (action == "set" && res == "(0)") {
    maxPosition = currentPosition;
    saveItNow = true;
  }

  /*
     Below are actions based on inbound MQTT payload
  */
  if (res == "(start)") {
    /*
       Store the current position as the start position
    */
    currentPosition = 0;
    path = 0;
    saveItNow = true;
    action = "manual";
  } else if (res == "(max)") {
    /*
       Store the max position of a closed blind
    */
    maxPosition = currentPosition;
    path = 0;
    saveItNow = true;
    action = "manual";
  } else if (res == "(0)") {
    /*
       Stop
    */
    path = 0;
    saveItNow = true;
    action = "manual";
  } else if (res == "(1)") {
    /*
       Move down without limit to max position
    */
    path = 1;
    action = "manual";
  } else if (res == "(-1)") {
    /*
       Move up without limit to top position
    */
    path = -1;
    action = "manual";
  } else if (res == "(update)") {
    //Send position details to client
    int set = (setPos * 100) / maxPosition;
    int pos = (currentPosition * 100) / maxPosition;
    sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
    webSocket.sendTXT(clientnum, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
  } else if (res == "(ping)") {
    //Do nothing
  } else {
    /*
       Any other message will take the blind to a position
       Incoming value = 0-100
       path is now the position
    */
    path = maxPosition * res.toInt() / 100;
    setPos = path; //Copy path for responding to updates
    action = "auto";

    int set = (setPos * 100) / maxPosition;
    int pos = (currentPosition * 100) / maxPosition;

    //Send the instruction to all connected devices
    sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
    webSocket.broadcastTXT("{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
  }
}

void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
  switch (type) {
    case WStype_TEXT:
      Serial.printf("[%u] get Text: %s\n", num, payload);

      String res = (char*)payload;

      //Send to common MQTT and websocket function
      processMsg(res, num);
      break;
  }
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String res = "";
  for (int i = 0; i < length; i++) {
    res += String((char) payload[i]);
  }
  processMsg(res, NULL);
}

/**
  Turn of power to coils whenever the blind
  is not moving
*/
void stopPowerToCoils() {
  digitalWrite(D1, LOW);
  digitalWrite(D2, LOW);
  digitalWrite(D3, LOW);
  digitalWrite(D4, LOW);
}

/*
   Callback from WIFI Manager for saving configuration
*/
void saveConfigCallback () {
  Serial.println("Should save config");
  shouldSaveConfig = true;
}

void setup(void)
{
  Serial.begin(115200);
  delay(100);
  Serial.print("Starting now\n");

  //Reset the action
  action = "";

  //Set MQTT properties
  outputTopic = getMqttTopic("out");
  inputTopic = getMqttTopic("in");
  mqttclientid = ("ESPClient-" + String(ESP.getChipId()));
  Serial.println("Sending to Mqtt-topic: " + outputTopic);
  Serial.println("Listening to Mqtt-topic: " + inputTopic);
  //Setup MQTT Client ID
  Serial.println("MQTT Client ID: " + String(mqttclientid));

  //Set the WIFI hostname
  WiFi.hostname(config_name);

  //Define customer parameters for WIFI Manager
  WiFiManagerParameter custom_config_name("Name", "Bonjour name", config_name, 40);
  WiFiManagerParameter custom_mqtt_server("server", "MQTT server (optional)", mqtt_server, 40);
  WiFiManagerParameter custom_mqtt_port("port", "MQTT port", mqtt_port, 6);

  //Setup WIFI Manager
  WiFiManager wifiManager;

  //reset settings - for testing
  //clean FS, for testing
  //SPIFFS.format();
  //wifiManager.resetSettings();

  wifiManager.setSaveConfigCallback(saveConfigCallback);
  //add all your parameters here
  wifiManager.addParameter(&custom_config_name);
  wifiManager.addParameter(&custom_mqtt_server);
  wifiManager.addParameter(&custom_mqtt_port);
  wifiManager.autoConnect(APid.c_str(), APpw.c_str());

  //Load config upon start
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }

  /* Save the config back from WIFI Manager.
      This is only called after configuration
      when in AP mode
  */
  if (shouldSaveConfig) {
    //read updated parameters
    strcpy(config_name, custom_config_name.getValue());
    strcpy(mqtt_server, custom_mqtt_server.getValue());
    strcpy(mqtt_port, custom_mqtt_port.getValue());

    //Save the data
    saveConfig();
  }

  /*
     Try to load FS data configuration every time when
     booting up. If loading does not work, set the default
     positions
  */
  loadDataSuccess = loadConfig();
  if (!loadDataSuccess) {
    currentPosition = 0;
    maxPosition = 2000000;
  }

  /*
    Setup multi DNS (Bonjour)
  */
  if (!MDNS.begin(config_name)) {
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");

  // Start TCP (HTTP) server
  server.begin();
  Serial.println("TCP server started");

  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);

  //Start websocket
  webSocket.begin();
  webSocket.onEvent(webSocketEvent);

  /* Setup connection for MQTT and for subscribed
    messages IF a server address has been entered
  */
  if (String(mqtt_server) != "") {
    Serial.println("Registering MQTT server");
    psclient.setServer(mqtt_server, String(mqtt_port).toInt());
    psclient.setCallback(mqttCallback);
  } else {
    mqttActive = false;
    Serial.println("NOTE: No MQTT server address has been registered. Only using websockets");
  }


  //Setup OTA
  {

    // Authentication to avoid unauthorized updates
    //ArduinoOTA.setPassword((const char *)"nidayand");

    ArduinoOTA.setHostname(config_name);

    ArduinoOTA.onStart([]() {
      Serial.println("Start");
    });
    ArduinoOTA.onEnd([]() {
      Serial.println("\nEnd");
    });
    ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    });
    ArduinoOTA.onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
    ArduinoOTA.begin();
  }
}

int tripCounter = 0;
void loop(void)
{
  //OTA client code
  ArduinoOTA.handle();

  //Websocket listner
  webSocket.loop();

  //MQTT client
  if (mqttActive) {
    if (!psclient.connected())
      reconnect();
    psclient.loop();
  }


  /**
    Storing positioning data and turns off the power to the coils
  */
  if (saveItNow) {
    saveConfig();
    saveItNow = false;

    /*
      If no action is required by the motor make sure to
      turn off all coils to avoid overheating and less energy
      consumption
    */
    stopPowerToCoils();

  }

  /**
    Manage actions. Steering of the blind
  */
  if (action == "auto") {
    /*
       Automatically open or close blind
    */
    if (currentPosition > path) {
      small_stepper.step(-1);
      currentPosition = currentPosition - 1;
    } else if (currentPosition < path) {
      small_stepper.step(1);
      currentPosition = currentPosition + 1;
    } else {
      path = 0;
      action = "";
      int set = (setPos * 100) / maxPosition;
      int pos = (currentPosition * 100) / maxPosition;
      webSocket.broadcastTXT("{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
      sendmsg(outputTopic, "{ \"set\":" + String(set) + ", \"position\":" + String(pos) + " }");
      Serial.println("Stopped. Reached wanted position");
      saveItNow = true;
    }

  } else if (action == "manual" && path != 0) {
    /*
       Manually running the blind
    */
    small_stepper.step(path);
    currentPosition = currentPosition + path;
  }

  /*
     After running setup() the motor might still have
     power on some of the coils. This is making sure that
     power is off the first time loop() has been executed
     to avoid heating the stepper motor draining
     unnecessary current
  */
  if (initLoop) {
    initLoop = false;
    stopPowerToCoils();
  }

  /**
    Serving the webpage
  */
  {
    // Check if a client has connected
    WiFiClient webclient = server.available();
    if (!webclient) {
      return;
    }
    Serial.println("New client");

    // Wait for data from client to become available
    while (webclient.connected() && !webclient.available()) {
      delay(1);
    }

    // Read the first line of HTTP request
    String req = webclient.readStringUntil('\r');

    // First line of HTTP request looks like "GET /path HTTP/1.1"
    // Retrieve the "/path" part by finding the spaces
    int addr_start = req.indexOf(' ');
    int addr_end = req.indexOf(' ', addr_start + 1);
    if (addr_start == -1 || addr_end == -1) {
      Serial.print("Invalid request: ");
      Serial.println(req);
      return;
    }
    req = req.substring(addr_start + 1, addr_end);
    Serial.print("Request: ");
    Serial.println(req);
    webclient.flush();

    String s;
    if (req == "/")
    {
      s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE html><html><head> <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\"/> <meta http-equiv=\"Pragma\" content=\"no-cache\"/> <meta http-equiv=\"Expires\" content=\"0\"/> <title>{NAME}</title> <link rel=\"stylesheet\" href=\"https://unpkg.com/onsenui/css/onsenui.css\"> <link rel=\"stylesheet\" href=\"https://unpkg.com/onsenui/css/onsen-css-components.min.css\"> <script src=\"https://unpkg.com/onsenui/js/onsenui.min.js\"></script> <script src=\"https://unpkg.com/jquery/dist/jquery.min.js\"></script> <script>var cversion=\"{VERSION}\"; var wsUri=\"ws://\"+location.host+\":81/\"; var repo=\"motor-on-roller-blind-ws\"; window.fn={}; window.fn.open=function(){var menu=document.getElementById('menu'); menu.open();}; window.fn.load=function(page){var content=document.getElementById('content'); var menu=document.getElementById('menu'); content.load(page) .then(menu.close.bind(menu)).then(setActions());}; var gotoPos=function(percent){doSend(percent);}; var instr=function(action){doSend(\"(\"+action+\")\");}; var setActions=function(){doSend(\"(update)\"); $.get(\"https://api.github.com/repos/nidayand/\"+repo+\"/releases\", function(data){if (data.length>0 && data[0].tag_name !==cversion){$(\"#cversion\").text(cversion); $(\"#nversion\").text(data[0].tag_name); $(\"#update-card\").show();}}); setTimeout(function(){$(\"#arrow-close\").on(\"click\", function(){$(\"#setrange\").val(100);gotoPos(100);}); $(\"#arrow-open\").on(\"click\", function(){$(\"#setrange\").val(0);gotoPos(0);}); $(\"#setrange\").on(\"change\", function(){gotoPos($(\"#setrange\").val())}); $(\"#arrow-up-man\").on(\"click\", function(){instr(\"-1\")}); $(\"#arrow-down-man\").on(\"click\", function(){instr(\"1\")}); $(\"#arrow-stop-man\").on(\"click\", function(){instr(\"0\")}); $(\"#set-start\").on(\"click\", function(){instr(\"start\")}); $(\"#set-max\").on(\"click\", function(){instr(\"max\");});}, 200);}; $(document).ready(function(){setActions();}); var websocket; var timeOut; function retry(){clearTimeout(timeOut); timeOut=setTimeout(function(){websocket=null; init();},5000);}; function init(){ons.notification.toast({message: 'Connecting...', timeout: 1000}); try{websocket=new WebSocket(wsUri); websocket.onclose=function (){}; websocket.onerror=function(evt){ons.notification.toast({message: 'Cannot connect to device', timeout: 2000}); retry();}; websocket.onopen=function(evt){ons.notification.toast({message: 'Connected to device', timeout: 2000}); setTimeout(function(){doSend(\"(update)\");}, 1000);}; websocket.onclose=function(evt){ons.notification.toast({message: 'Disconnected. Retrying', timeout: 2000}); retry();}; websocket.onmessage=function(evt){try{var msg=JSON.parse(evt.data); if (typeof msg.position !=='undefined'){$(\"#pbar\").attr(\"value\", msg.position);}; if (typeof msg.set !=='undefined'){$(\"#setrange\").val(msg.set);};}catch(err){}};}catch (e){ons.notification.toast({message: 'Cannot connect to device. Retrying...', timeout: 2000}); retry();};}; function doSend(msg){if (websocket && websocket.readyState==1){websocket.send(msg);}}; window.addEventListener(\"load\", init, false); window.onbeforeunload=function(){if (websocket && websocket.readyState==1){websocket.close();};}; </script></head><body><ons-splitter> <ons-splitter-side id=\"menu\" side=\"left\" width=\"220px\" collapse swipeable> <ons-page> <ons-list> <ons-list-item onclick=\"fn.load('home.html')\" tappable> Home </ons-list-item> <ons-list-item onclick=\"fn.load('settings.html')\" tappable> Settings </ons-list-item> <ons-list-item onclick=\"fn.load('about.html')\" tappable> About </ons-list-item> </ons-list> </ons-page> </ons-splitter-side> <ons-splitter-content id=\"content\" page=\"home.html\"></ons-splitter-content></ons-splitter><template id=\"home.html\"> <ons-page> <ons-toolbar> <div class=\"left\"> <ons-toolbar-button onclick=\"fn.open()\"> <ons-icon icon=\"md-menu\"></ons-icon> </ons-toolbar-button> </div><div class=\"center\">{NAME}</div></ons-toolbar><ons-card> <div class=\"title\">Adjust position</div><div class=\"content\"><p>Move the slider to the wanted position or use the arrows to open/close to the max positions</p></div><ons-row> <ons-col width=\"40px\" style=\"text-align: center; line-height: 31px;\"> </ons-col> <ons-col> <ons-progress-bar id=\"pbar\" value=\"75\"></ons-progress-bar> </ons-col> <ons-col width=\"40px\" style=\"text-align: center; line-height: 31px;\"> </ons-col> </ons-row> <ons-row> <ons-col width=\"40px\" style=\"text-align: center; line-height: 31px;\"> <ons-icon id=\"arrow-open\" icon=\"fa-arrow-up\" size=\"2x\"></ons-icon> </ons-col> <ons-col> <ons-range id=\"setrange\" style=\"width: 100%;\" value=\"25\"></ons-range> </ons-col> <ons-col width=\"40px\" style=\"text-align: center; line-height: 31px;\"> <ons-icon id=\"arrow-close\" icon=\"fa-arrow-down\" size=\"2x\"></ons-icon> </ons-col> </ons-row> </ons-card> <ons-card id=\"update-card\" style=\"display:none\"> <div class=\"title\">Update available</div><div class=\"content\">You are running <span id=\"cversion\"></span> and <span id=\"nversion\"></span> is the latest. Go to <a href=\"https://github.com/nidayand/motor-on-roller-blind-ws/releases\">the repo</a> to download</div></ons-card> </ons-page></template><template id=\"settings.html\"> <ons-page> <ons-toolbar> <div class=\"left\"> <ons-toolbar-button onclick=\"fn.open()\"> <ons-icon icon=\"md-menu\"></ons-icon> </ons-toolbar-button> </div><div class=\"center\"> Settings </div></ons-toolbar> <ons-card> <div class=\"title\">Instructions</div><div class=\"content\"> <p> <ol> <li>Use the arrows and stop button to navigate to the top position i.e. the blind is opened</li><li>Click the START button</li><li>Use the down arrow to navigate to the max closed position</li><li>Click the MAX button</li><li>Calibration is completed!</li></ol> </p></div></ons-card> <ons-card> <div class=\"title\">Control</div><ons-row style=\"width:100%\"> <ons-col style=\"text-align:center\"><ons-icon id=\"arrow-up-man\" icon=\"fa-arrow-up\" size=\"2x\"></ons-icon></ons-col> <ons-col style=\"text-align:center\"><ons-icon id=\"arrow-stop-man\" icon=\"fa-stop\" size=\"2x\"></ons-icon></ons-col> <ons-col style=\"text-align:center\"><ons-icon id=\"arrow-down-man\" icon=\"fa-arrow-down\" size=\"2x\"></ons-icon></ons-col> </ons-row> </ons-card> <ons-card> <div class=\"title\">Store</div><ons-row style=\"width:100%\"> <ons-col style=\"text-align:center\"><ons-button id=\"set-start\">Set Start</ons-button></ons-col> <ons-col style=\"text-align:center\">&nbsp;</ons-col> <ons-col style=\"text-align:center\"><ons-button id=\"set-max\">Set Max</ons-button></ons-col> </ons-row> </ons-card> </ons-page></template><template id=\"about.html\"> <ons-page> <ons-toolbar> <div class=\"left\"> <ons-toolbar-button onclick=\"fn.open()\"> <ons-icon icon=\"md-menu\"></ons-icon> </ons-toolbar-button> </div><div class=\"center\"> About </div></ons-toolbar> <ons-card> <div class=\"title\">Motor on a roller blind</div><div class=\"content\"> <p> <ul> <li>3d print files and instructions: <a href=\"https://www.thingiverse.com/thing:2392856\">https://www.thingiverse.com/thing:2392856</a></li><li>MQTT based version on Github: <a href=\"https://github.com/nidayand/motor-on-roller-blind\">https://github.com/nidayand/motor-on-roller-blind</a></li><li>WebSocket based version on Github: <a href=\"https://github.com/nidayand/motor-on-roller-blind-ws\">https://github.com/nidayand/motor-on-roller-blind-ws</a></li><li>Licensed unnder <a href=\"https://creativecommons.org/licenses/by/3.0/\">Creative Commons</a></li></ul> </p></div></ons-card> </ons-page></template></body></html>\r\n\r\n";
      s.replace("{VERSION}", "V" + version);
      s.replace("{NAME}", String(config_name));
      Serial.println("Sending 200");
    }
    else
    {
      s = "HTTP/1.1 404 Not Found\r\n\r\n";
      Serial.println("Sending 404");
    }

    //Print page but as max package is 2048 we need to break it down
    while (s.length() > 2000) {
      String d = s.substring(0, 2000);
      webclient.print(d);
      s.replace(d, "");
    }
    webclient.print(s);
}
}

 

 

After you uploaded the code to your NodeMcu v3 Lua ESP-12E WIFI Development Board, go to your WIFI settings on your computer. Connect your computer to a new WIFI hotspot named:

AutoConnectAP

and use the Password:

StudioPieters

You will be redirected to this screen and now you can select Configure WiFi.

WOW, How does it works!

When your ESP starts up, it sets it up in Station mode and tries to connect to a previously saved Access Point if this is unsuccessful (or no previous network saved) it moves the ESP into Access Point mode and spins up a DNS and WebServer (default ip 192.168.4.1) using any wifi enabled device with a browser (computer, phone, tablet) connect to the newly created Access Point. Because of the Captive Portal and the DNS server you will either get a ‘Join to network’ type of popup or get any domain you try to access redirected to the configuration portal choose one of the access points scanned, enter password, click Save. The ESP will try to connect. If successful, it relinquishes control back to your app. If not, reconnect to AP and reconfigure.

After you configured your NodeMcu v3 Lua ESP-12E WIFI Development Board, you can connect to your normal WIFI with your computer, phone or tablet and go to the IP address of the device – or if you have an mDNS supported device (e.g. iOS, OSX or have Bonjour installed) you can go to http://{hostname}.local. If you don’t know the IP-address of the device check your router for the leases or more simple check the serial console in the Arduino IDE.

Using MQTT

When it connects to WIFI and MQTT it will send a “register” message to topic /raw/esp8266/register with a payload containing chip-id and IP-address. A message to /raw/esp8266/[chip-id]/in will steer the blind according to the “payload actions” below. Updates from the device will be sent to topic /raw/esp8266/[chip-id]/out

If you don’t want to use MQTT. Simply do not enter any string in the MQTT server form field upon WIFI configuration of the device.

Configuring your blinds settings

Now that everything is installed it’s time to calibrate you Blinds. Open the webpage typing the IP address in your web browser, that is assigned to your NodeMcu v3 Lua ESP-12E WIFI Development Board. As the webpage is loaded it will connect through a websocket directly to the device to progress updates and to control the device. If any other client connects the updates will be in sync. Go to the Settings page to calibrate the motor with the start and end positions of the roller blind. Follow the instructions on the page.

 

After the calibration of your blinds, you can If you don’t Use MQTT, Use Web interface to open and close the blinds or use the slider to set it on a manual position.

If you want to use MQTT or  in this case Homebridge to control your blinds you need to configure your homebridge to connect to your MQTT Blinds.

Homekit2mqtt webserver

To add the new accessories, homekit2mqtt has a build-in web-server. This allows you that easily add new MQTT accessories for Homekit. So to connect to your homekit2mqtt web-server, open your web browser and type:

Raspberry_ip_adress:51888

Now you can login with the username:

homekit

and password:

031-45-154



For this setup I added a accessories to the Apple Homebridge based on MQTT by removing all standard MQTT clients by selecting them and press DEL in the lower left corner.

Then you press + Add, to add my new build roller blind control. In the service field you can select your type of service, in this case want want to add a window covering.

In the Template field you can select a predefined template , in this case we choose None.

In the configuration screen you can fill in the following fields:

ID Check the serial console in the Arduino for the esp8266 ID
Name The name you want to give it homekit, eg. Rollerblind
SetTargetPosition /raw/esp8266/ID/in
StatusTargetPosition /raw/esp8266/ID/out
StatusCurrentPosition /raw/esp8266/ID/in
StatusPositionState /raw/esp8266/ID/in
Identify

At The MQTT Payloads and fill in the following fields:

targetPositionFactor Number 1
currentPositionFactor Number 1
positionStatusDecreasing Number 1
positionStatusIncreasing Number 1

At the accessoryInformation You can add the Manufacturer model and serial number, but it’s not necessary. Then select Save and close the web browser.

So your InTopic is in this case /raw/esp8266/CHIP-ID/.

3D model

Now that we have the hardware working we need a enclosure for the motor. You can download all files at the end of this article.

TESTING FASE

After installing my blind with the standard 5V 28BYJ-48 Stepper Motor. It work kindoff… because my blind are 2 meter wide and needs to go down proximally 2,5 meters the motor isn’t strong enough. So I’ve ordered a new 12V 28BYJ-48 Stepper Motor, and replaced the 5V one. This also required another 12 V power supply. Unfornually this motor also has not enough torque to open the blinds once they were open.

 

So back to the drawing board and figure a way out to get this thing working with bigger blinds. Want to see how to fix this problem, come back soon, for part two!

 

 
DOWNLOAD ALL FILES FOR THIS PROJECT ON GITHUB.
AND DOWNLOAD THEM FORM THINGIVERSE HERE.
           
DO YOU HAVE ANY QUESTIONS? LEAVE A COMMENT DOWN BELOW!

Reference:
Peter nidayand ( Okt 4, 2017) motor-on-roller-blind, WebSocket based version of motor-on-roller-blind, https://github.com/nidayand/motor-on-roller-blind ; Thomas O Fredericks (Nov 21, 2016), Stepper 28BYJ_48 UL2003 Library for Arduino/Wiring, Stepper Library for 5V Stepper Motors 28BYJ-48 with ULN2003 Driver, https://github.com/thomasfredericks/Stepper_28BYJ_48; ESP8266 Community Forum (Dec 18, 2017), Arduino core for ESP8266 WiFi chip, Support for ESP8266 chip to the Arduino environment. , https://github.com/esp8266/Arduino ; Nick O’Leary (Jun 7, 2017), Arduino Client for MQTT, This library provides a client for doing simple publish/subscribe messaging with a server that supports MQTT., https://github.com/knolleary/pubsubclient; Tzapu (Aug 22, 2017), WiFiManager, ESP8266 WiFi Connection manager with fallback web configuration portal,https://github.com/tzapu/WiFiManager; Benoît Blanchon (Dec 15, 2017), ArduinoJson, ArduinoJson is a C++ JSON library for Arduino and IoT (Internet Of Things), https://github.com/bblanchon/ArduinoJson; Markus ( Okt 16, 2017), arduinoWebSockets, a WebSocket Server and Client for Arduino based on RFC6455., https://github.com/Links2004/arduinoWebSockets Sebastian Raff (APR 25 2018), Homekit2MQTT, Use to control a  MQTT-connected “Thing” in your home automation through Siri and with HomeKit apps., https://github.com/hobbyquaker/homekit2mqtt