Horizontal Blinds Automation

Hardware

The Research

When I first started this project I first looked for commercially available hardware that would do what I want: control the tilt and height of my blinds. Most automated blind solutions fall into three categories:

  1. Roll-up blinds: These blinds are like what you would see in a storefront where a sheet of material is on a spool and can be raised/lowered with a chain
  2. Horizontal blinds that use a pull loop to accomplish tilting the tilting
  3. Horizontal blinds that use a control wand to accomplish the tilting

Categories 2 and 3 are both for horizontal blinds but didn’t fit my needs as my blinds look similar to these. There are two pull cords for the tilt adjustment: one moves the blinds one way and the other moves them the opposite. None of the solutions were suitable for my situation; on top of this they were unable to raise or lower the blinds. Even if they were suitable the price was pretty high: $75-250 depending on the solution!

I decided to roll my own using inexpensive parts to test everything and so far so good. Each set of blinds that will be retrofitted will have:

  • 1 ESP8826 microcontroller
  • 1 995 metal gear servo
  • 3 roller lever microswitches
  • 1 12V – 5V DC buck converter
  • 1 Dual-channel H-bridge
  • 2 12VDC gear motor
  • 2 pulleys
  • some 3d printed parts

I chose a NodeMCU to do the prototyping with but will probably end up using a WeMos D1 mini as the final platform. The microswitches will be used to allow local control of the blinds in locations and functions similar to the original controls.

Investigation

I pulled a set of blinds down to see what I could do and found that the tilt was controlled by a little worm and wheel mechanism that turned a 1/4″ rod that ran the width of the blinds. This in turn pulled on one of two strings to control the angle of the ladder that the blind slats lie in. The body of the top  of the blinds is made from  bent sheet metal that is shaped into a U. I designed and 3d printed a bracket to hold the servo in the proper position as well as a servo horn adapter to attach to the rod that controls the tilt. In this picture you can also see an earlier iteration of the switch and actuator arm that will be used to control the tilt electronically.

Here’s a different look at that mechanism. A string will go through the body and attach to the arm. Pulling the string will depress the roller lever on the switch and open a connection on the microcontroller.

Integration

The next step was to wire it all up and program it to work. Cue nest of wiring:

Description of the solution

The easiest way to integrate DIY solutions into Home Assistant is via MQTT, a protocol for IoT(Internet of Things) devices. MQTT works as a pub/sub broker. The implementation I’m using is for my broker is Mosquitto running on a CentOS 7 platform. The NodeMCU platform has Wi-Fi hardware built in and is relatively easy to get sending/receiving messages via MQTT. I also needed a switch to enable local control of the hardware.

Implementation of the solution

I decided to use four different MQTT topics one each for tilt command, tilt status, raise/lower command, raise/lower status. As of now I’m still waiting on parts for the raise/lower and haven’t figured out how I’m going to handle status. The program expects to receive a value from 0-180 on the tilt command topic and will send this value to the servo to tilt the blinds to the appropriate position. The method that actually controls moving the servo also handles updating the tilt status topic with the current position. This made adding the switch to control simple.

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <Servo.h>
// Update these with values suitable for your network.
const char* ssid = "";
const char* password = "";
const char* mqtt_server = "";
const char* mqtt_user = "";
const char* mqtt_password = "";

const int TILT_SERVO_PIN = D1;
const int TILT_SWITCH_PIN = D2;
const int UP_SWITCH_PIN = D3;
const int DOWN_SWITCH_PIN = D4;
const int MOTOR_UP_PIN = D5;
const int MOTOR_DOWN_PIN = D6;
const int MOTOR_ENABLE_PIN = D7;
const bool RAISE = true;
const bool LOWER = false;
const int positions[] = {20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170};
const int POSITIONS_SIZE = 11;

const char* IDENTIFIER = "livingRoom0";
const char* STATUS_TOPIC = "livingRoom0/status";
const char* TILT_TOPIC = "livingRoom0/tilt";
const char* RAISE_TOPIC = "livingRoom0/raise";

int tiltPosition = 0;
int currentPositionIndex = 0;
bool increase = true;
bool drivingMotors = false;



Servo myservo;
WiFiClient espClient;
PubSubClient client(espClient);

void setup_wifi() {
   delay(100);
  // We start by connecting to a WiFi network
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
  randomSeed(micros());
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void moveIt(int position, bool report) {
  myservo.attach(TILT_SERVO_PIN);  // attaches the servo on pin D1 to the servo object
  myservo.write(position);

  if (report) {
    tiltPosition = position;
    char message[100];
    sprintf(message, "%d", position);
    const char* toSend = message;
    Serial.print("Publishing message: ");
    Serial.println(toSend);
    client.publish(STATUS_TOPIC, toSend, false);
  }
  delay(500);
  myservo.detach();
}

void moveIt(int position) {
  moveIt(position, true);
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Received: ");
  Serial.println((char *)payload);
  Serial.print("Command with length: ");
  Serial.print(length);
  Serial.print(" from MQTT broker is : [");
  Serial.print(topic);
  Serial.print("] value: ");
  String value = "";
  for(int i=0;i<length;i++) {
    value = value +  (char)payload[i];    
  }
  if (value != "") {
    if (strcmp(topic, TILT_TOPIC) == 0) {
      Serial.println(value);
      moveIt(value.toInt());
      tiltPosition = value.toInt();
    } else if (strcmp(topic, RAISE_TOPIC) == 0) {
      Serial.println("Raise command received: " + value);
    }
  }
}//end callback

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "ESP8266Client-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
      Serial.println("connected");
     //once connected to MQTT broker, subscribe command if any
      client.subscribe(TILT_TOPIC);
      client.subscribe(RAISE_TOPIC);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 6 seconds before retrying
      delay(6000);
    }
  }
} //end reconnect()

void setup() {
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  pinMode(TILT_SWITCH_PIN, INPUT_PULLUP);
  pinMode(UP_SWITCH_PIN, INPUT_PULLUP);
  pinMode(DOWN_SWITCH_PIN, INPUT_PULLUP);
  pinMode(MOTOR_UP_PIN, OUTPUT);
  pinMode(MOTOR_DOWN_PIN, OUTPUT);
  pinMode(MOTOR_ENABLE_PIN, OUTPUT);
  digitalWrite(MOTOR_ENABLE_PIN, HIGH);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();
  int tiltStatus = digitalRead(TILT_SWITCH_PIN);

  if (tiltStatus == LOW && !drivingMotors) {
      if (increase) {
        currentPositionIndex = currentPositionIndex + 1;
        if (currentPositionIndex > POSITIONS_SIZE - 1) {
          increase = false;
          currentPositionIndex -= 2;
        }
      } else {
        currentPositionIndex = currentPositionIndex - 1;
        if (currentPositionIndex < 0) {
          increase = true;
          currentPositionIndex += 2;
        }
      }
      Serial.print("Current position index is ");
      Serial.println(currentPositionIndex);
      
      moveIt(positions[currentPositionIndex]);
  }

  int upSwitchStatus = digitalRead(UP_SWITCH_PIN) == LOW;
  int downSwitchStatus = digitalRead(DOWN_SWITCH_PIN) == LOW;
  if (upSwitchStatus && !downSwitchStatus) {
    Serial.println("Raising");
    raiseOrLower(RAISE);
  } else if (downSwitchStatus && !upSwitchStatus) {
    Serial.println("Lowering");
    raiseOrLower(LOWER);
  } else if (!upSwitchStatus && !downSwitchStatus) {
    if (drivingMotors) {
      drivingMotors = false;
      digitalWrite(MOTOR_UP_PIN, LOW);
      digitalWrite(MOTOR_DOWN_PIN, LOW);
      moveIt(tiltPosition, false);
    }
  }
}

void raiseOrLower(bool isRaise) {
  if (!drivingMotors) {
    moveIt(80, false);
    drivingMotors = true;
  } else {
    if (isRaise) {
      digitalWrite(MOTOR_UP_PIN, HIGH);
      digitalWrite(MOTOR_DOWN_PIN, LOW);
    } else {
      digitalWrite(MOTOR_UP_PIN, LOW);
      digitalWrite(MOTOR_DOWN_PIN, RAISE);
    }
  }
}