[Project] Venetian blinds (ISSO) – esphome + WeMos D1 mini + 28byj-48 stepper

I have many windows with ISSO style venetian blinds (type with ball chain pull). For some time I wanted to automate them but of the shelf solutions would get quite expensive just due to number blinds needed to be automated (just for bedroom I need six of them).

I looked at some DYI options using chain puller wheels but those were chunky and not very reliable. Therefore, I have decided to make my own try and here is a result.

I have decided to integrate everything into blinds holder and replace integrated pull cord assembly. I designed new blinds holder and motor + electronics mount. This allowed me to do 1:1 swap manual->electronic, which is reversible.

Before

After

I am using 28byj-48 stepper motor running on 5V (USB supply powers everything), which is quite cheap but low torque. For my blinds, it is sufficient but for bigger ones change to 9-12V or rewiring to bipolar setup may be necessary. This motor drives via right angle 1:1 helical gears blinds shaft. Motor is driven by ULN2003 (came in set with motor) from WeMos D1 mini. I have mounted everything on custom PCB for easier assembly and to hold push buttons.

Blinds holder and shaft with helical gear

Motor and electronics

An ESPHome program takes care of driving the blind and integration into Home Assistant.

The blinds are running for several months without any major issue. At first, I have been worried about printed gears but they hold fine. I had to replace one of them but it was not fault of design – I used low infill and put the gear only half way on shaft, which resulted in splitting.

If somebody is interested, I can post somewhere STL files.

esphome:
  name: TestCover
  platform: ESP8266
  board: d1_mini

  on_boot:
    then:
      - cover.open: zaluzie   


wifi:
  ssid: "xxxxxxxxx"
  password: "xxxxxxxxxxxxxxxxxxx"

# Enable logging
logger:
  
# Enable Home Assistant API
api:
  services:
    - service: control_stepper
      variables:
        target: int
      then:
        - stepper.set_target:
            id: my_stepper
            target: !lambda 'return target;'

ota:

time:
  - platform: homeassistant
    id: homeassistant_time

#web_server:
#  port: 80 

globals:
 - id: cover_steps
   type: int
   restore_value: no
   initial_value: '59000'

 - id: last_pos
   type: float
   restore_value: no
   initial_value: '1'

 - id: extra_ups
   type: int
   restore_value: no
   initial_value: '2'

 - id: extra_ups_remaining
   type: int
   restore_value: no
   initial_value: '2'


# physical connection
stepper: #https://community.home-assistant.io/t/motor-on-a-roller-blind-esphome-version/116179/11
  - platform: uln2003
    id: my_stepper
    pin_a: GPIO15
    pin_b: GPIO13
    pin_c: GPIO12
    pin_d: GPIO14
    max_speed: 250 steps/s
    sleep_when_done: true    
    acceleration: inf
    deceleration: inf
    step_mode: FULL_STEP


sensor:
  - platform: wifi_signal
    name: "WiFi signal"
    update_interval: 60s


binary_sensor:
  - platform: gpio
    pin:
      number: D1
      mode: INPUT_PULLUP
      inverted: True
    name: "Up"
    id: button_up
    on_press:
      then:
        - if:
            condition:
              lambda: 'return ((id(my_stepper).current_position) > (id(my_stepper).target_position));'
            then:
              - cover.stop: zaluzie
            else:
              - if:
                  condition:
                    lambda: 'return ((id(my_stepper).current_position) == id(cover_steps));'
                  then:
                    lambda: 'id(my_stepper).report_position (id(cover_steps)-500);'
              - cover.open: zaluzie   

  - platform: gpio
    pin:
      number: D2
      mode: INPUT_PULLUP
      inverted: True
    name: "Stop"   
    id: button_stop
    on_press:
      - cover.stop: zaluzie    
    
  - platform: gpio
    pin:
      number: D3
      mode: INPUT_PULLUP
      inverted: True
    name: "Down"  
    id: button_down
    on_press:
      then:
        - if:
            condition:
              lambda: 'return ((id(my_stepper).current_position) < (id(my_stepper).target_position));'
            then:
              - cover.stop: zaluzie
            else:
              - if:
                  condition:
                    lambda: 'return ((id(my_stepper).current_position) == 0);'
                  then:
                    lambda: 'id(my_stepper).report_position (500);'
              - cover.close: zaluzie   

interval:
  - interval: 5s
    then:
      - cover.template.publish:
          id: zaluzie
          position: !lambda |-
            if (((id(my_stepper).current_position) == (id(cover_steps))) && ((id(my_stepper).current_position) == (id(my_stepper).target_position)) && (id(extra_ups_remaining))>0) {
                id(extra_ups_remaining) = id(extra_ups_remaining) - 1;
                id(my_stepper).report_position (id(cover_steps)-500);
                id(my_stepper).set_target(id(cover_steps)); 
            }  
            id(last_pos) = (((id(my_stepper).current_position * 1.0) / (1.0 * id(cover_steps)))); // calculate real position and store it
            return id(last_pos);
            
          current_operation: !lambda |-
            if ((id(my_stepper).current_position) == (id(my_stepper).target_position)) {
              return COVER_OPERATION_IDLE;
            } else if ((id(my_stepper).current_position) > (id(my_stepper).target_position)) {
              return COVER_OPERATION_CLOSING;
            } else {
              return COVER_OPERATION_OPENING;
            }
   

# items for Home Assistant 
cover:
  - platform: template
    name: "Test cover"
    id: zaluzie
    has_position: true
#    lambda: 'return (((id(my_stepper).current_position * 1.0) / (1.0 * id(cover_steps))));'

    open_action:
      - stepper.set_target:
          id: my_stepper
          target: !lambda return id(cover_steps);
      - cover.template.publish:
          id: zaluzie
          position: !lambda |-
            id(last_pos) = (((id(my_stepper).current_position * 1.0) / (1.0 * id(cover_steps)))); // calculate real position and store it
            return id(last_pos);
            
          current_operation: !lambda |-
              return COVER_OPERATION_OPENING;

    close_action:
      - stepper.set_target:
          id: my_stepper
          target: 0
      - cover.template.publish:
          id: zaluzie
          position: !lambda |-
            id(last_pos) = (((id(my_stepper).current_position * 1.0) / (1.0 * id(cover_steps)))); // calculate real position and store it
            return id(last_pos);
            
          current_operation: !lambda |-
              return COVER_OPERATION_CLOSING;

    stop_action:
      - stepper.set_target:
          id: my_stepper
          target: !lambda return (id(my_stepper).current_position);      
      - cover.template.publish:
          id: zaluzie
          position: !lambda |-
            id(last_pos) = (((id(my_stepper).current_position * 1.0) / (1.0 * id(cover_steps)))); // calculate real position and store it
            return id(last_pos);
          current_operation: !lambda |-
              return COVER_OPERATION_IDLE;

    optimistic: false  
    


# for manual adjustment of the initial setting position
switch:
  - platform: template
    name: "Close"
    turn_on_action:
      - cover.close: zaluzie   
  - platform: template
    name: "Open"
    turn_on_action:
      - cover.open: zaluzie
      

8 Likes

Wow this is really awesome masterpiece !!

thx for your great DIY skill open.

Looks inspiring - thanks for sharing!

I assume you are using to open/close the blinds and not lift them up to the top of the window?

Also, can you confirm the approximate dimensions of the blind you have your motors working on (max size a 28byj-48 is proven to handle)

Pretty sure I have some of these steppers in my “box of interesting things I might do something with” - so is great to have a solid go-by!

Phil

I’m using motor to lift them up and lower too. My blinds are quite small - 38 cm (15") wide and 114 cm (45") heigh. The 28byj-48 can handle them but barely. This size of blinds is clearly motor limit on 5V.
If you have bigger blinds you have to fo for higher voltage or convert motor to bipolar and use different stepper driver.

At the moment I’m using motor without modification on 5V as it is sufficient for me. I have PCB
https://easyeda.com/zdar/blinds-driver
which holds everything. Note: there is space for IR receiver on boards but it is untested.

I have similar PCB (same form factor) with A4988 (POLOLU) for bipolar setup in preparation
https://easyeda.com/zdar/blinds-driver_copy
but I have not tested that yet.

1 Like

This may be a stupid question by how are the limits of the motor set? Do you use the switches in home assistant, and then press the stop button once its gone as far as you want it to?

@jack465

It is quite easy. Everything is done inside ESPhome without need of HA intervention.

HA -> ESPhome -> cover component -> stepper component -> stepper driver+motor

In short I’m setting desired stepper component position depending on requested cover position. ESPhome will then rotate stepper (a lot of revolutions) to get to that position. The cover position is reported back based on stepper component position.

I have set stepper component limits (inside EPShome - variable “cover_steps”) to represent full travel of real cover from open to close or vice versa. That means ESPhome will not rotate motor more. It acts as soft stop switch.
I’m not using any hardware stop swithes. If there is mismatch between ESPhome and real position of the blind it is not that big deal - simply roll it up. When the limits of blind are reached motor will be jammed - sofar there was not any problem with that. I’m taking advantage of that as I’m doing full retraction on startup to get everything in sync. I have also in code included some extra steps up to compensate for blinds with problems (I had few issues before getting hardware tuned).

There is one small problem with EPShome cover implementation. Current release does not allow to set exact cover position from HA GUI. You have to manually stop it at that position. However fix is in development version and once it is released it will be easy to update the code.

1 Like

Hi, this is awesome project. Im thinking about using some chain puller, but this is much better. Please can you share the stl files? Thanks you very much.

Hi,
the stl files can be found here:
https://www.prusaprinters.org/prints/40684-motorized-venetian-blinds/files

If you needed fusion360 files PM me.

1 Like

This is an excellent project and tutorial. I really appreciate you sharing this info with us. I have a question. Are you powering the motor from the 5v pin of the D1 mini? I have experience that the D1 mini gets fried when powering servos. This is my first time using them with the 28BYJ-48 motor. Thanks again for this post!

Are you powering the motor from the 5v pin of the D1 mini?

No. It is the other way. I’m routing 5V trough my breakout board with motor “controller” to D1 mini (via 5V PIN). See the PCB in https://easyeda.com/zdar/blinds-driver
The PCB is even designed with dual power in mind - you can provide 5V to D1 and 12V to motor.

1 Like

Hi I want to ask you about few things about your technical solution. Is there any chance that this topic is still active? :slight_smile:

Yes :slight_smile:

P.S.: I’m using them regularly since then

Nemluvíš náhodou česky? Zajímalo by mě, jak máš vyřešené napájení… :slight_smile:

Já to vyřešil microUSB konektorem . Vedu v něm 5 i 12 V.

What truly amazes me is how you used programming language such as “then” where did you find the “who to” allowing you to know how to use these programming tricks and tips used in your YAML?