Motor on a roller blind - ESPHome version?

Thx. This helped me alot

Thanks Richard!

Can you share some details? Which parts do you use, and how is it mounted? Is the motor mounted at one end, and the rotary encoder at the other? I have everything working with some 28BYJ-48 on 12V, but I find them way to slow since the maximum speed I can set them to without skipping steps is 450 steps.

All the essential details are above. I 3D printed a housing and added the magnets to the rotating blind end. I’ve since found motors like this and I’d use one of these if I did this again https://www.ebay.co.uk/itm/Reversible-GW4058-31ZY-Worm-Gear-Motor-Self-locking-High-Torque-With-Hall-Drive/274534578568

Hope that helps.

1 Like

thanks for the code. Let me know if you figure out the stuttering.

Unfortunately not, but it doesn’t bother me anymore. I also added position support since Esphome was updated. And also added an open offset, since for me at least, I had some problems that my motor would skip when opening, so the blind was not exactly in the correct physical position, and when closing again, it would sometimes go too much. This offset makes sure that when opening the blind, it is open with slightly more steps than needed, but these extra steps are not taken into account for the current position.
Here is the updated code, it also contains a switch for a relay controlling my heating, since the esp is close to that. You can ignore that part. (left more details below the code if interested)

esphome:
  name: kitchen_blind
  platform: ESP8266
  board: nodemcuv2
  on_boot:
    priority: -100
    then:
      - stepper.report_position:
          id: blind_stepper
          position: !lambda "return id(current_position);"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(current_position);"
      - stepper.set_speed:
          id: blind_stepper
          speed: 400 steps/s
      - cover.template.publish:
          id: kitchen_blind
          current_operation: IDLE
          position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position))));'
      - output.turn_on: builtin_led
  esp8266_restore_from_flash: true

wifi:
  ssid: "xxxxx"
  password: "xxxxx"
  manual_ip:
    static_ip: 192.168.1.3
    gateway: 192.168.1.1
    subnet: 192.168.1.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Kitchen Blind Fallback Hotspot"
    password: !secret esphome_password
    ap_timeout: 1min
  reboot_timeout: 2min
  use_address: 192.168.1.3

captive_portal:

web_server:
  port: 80
  # auth:
  #     username: admin
  #     password: !secret haLocalKey

# Enable logging
logger:

ota:
  password: !secret esphome_password

# Enable Home Assistant API
api:
  password: !secret esphome_password

script:
- id: turn_on_heating
  mode: restart
  then:
    - switch.turn_on: heating_relay
    - delay: 5 min
    - switch.turn_off: heating_relay
- id: turn_off_heating
  mode: restart
  then:
    - switch.turn_off: heating_relay

globals:
  - id: open_position
    type: int
    initial_value: '9400'
  - id: open_position_offset
    type: int
    initial_value: '65'
  - id: middle_position
    type: int
    initial_value: '3760'
  - id: current_position
    type: int
    initial_value: '9400'
    restore_value: true
  - id: temp_position
    type: int
    initial_value: '0'

output:
  - platform: gpio
    pin: GPIO2
    id: builtin_led

switch:
  - platform: gpio
    name: "Heating Relay"
    id: heating_relay
    pin:
      number: D7
      inverted: true
    restore_mode: ALWAYS_OFF
  - platform: template
    name: "Heating Switch"
    id: heating_switch
    lambda: |-
        return false;
    assumed_state: true
    turn_on_action:
      - script.execute: turn_on_heating
    turn_off_action:
      - script.execute: turn_off_heating

stepper:
  - platform: a4988
    id: blind_stepper
    step_pin: D3
    dir_pin: D4
    max_speed: 800 steps/s
    sleep_pin:
      number: D2
      inverted: yes
    acceleration: inf
    deceleration: inf

status_led:
  pin: D1
  
cover:
  - platform: template
    device_class: shade
    name: Kitchen Blind
    id: kitchen_blind
    open_action:
      - stepper.set_speed:
          id: blind_stepper
          speed: 400 steps/s
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(open_position) + id(open_position_offset);"
      - while:
          condition:
            lambda: |-
              return id(kitchen_blind).position != 1;
          then:
            - cover.template.publish:
                id: kitchen_blind
                current_operation: !lambda |-
                    return COVER_OPERATION_OPENING;
                position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position) + id(open_position_offset))));'
            - delay: 1000 ms
      - cover.template.publish:
          id: kitchen_blind
          current_operation: IDLE
          position: !lambda 'return 1;'
      - stepper.report_position:
          id: blind_stepper
          position: !lambda "return id(open_position);"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(open_position);"
      - globals.set:
          id: current_position
          value: !lambda 'return id(open_position);'
      - output.turn_on: builtin_led
    close_action:
      - stepper.set_speed:
          id: blind_stepper
          speed: 800 steps/s
      - stepper.set_target:
          id: blind_stepper
          target: 0
      - while:
          condition:
            lambda: |-
              return id(kitchen_blind).position != 0;
          then:
            - cover.template.publish:
                id: kitchen_blind
                current_operation: !lambda |-
                    return COVER_OPERATION_CLOSING;
                position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position))));'
            - delay: 1000 ms
      - cover.template.publish:
          id: kitchen_blind
          current_operation: IDLE
          position: !lambda 'return 0;'
      - globals.set:
          id: current_position
          value: !lambda 'return 0;'
      - output.turn_on: builtin_led
    stop_action:
      - stepper.set_target:
          id: blind_stepper
          target: !lambda return id(blind_stepper).current_position;
      - cover.template.publish:
          id: kitchen_blind
          current_operation: IDLE
          position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position))));'
      - globals.set:
          id: current_position
          value: !lambda 'return id(blind_stepper).current_position;'
      - output.turn_on: builtin_led
    position_action:
      - globals.set:
          id: temp_position
          value: !lambda 'return float(float(id(open_position)) * pos);'
      - stepper.set_speed:
          id: blind_stepper
          speed: !lambda |-
            if (id(temp_position) >= id(blind_stepper).current_position) {
              return 400;
            } else {
              return 800;
            }
      - stepper.set_target:
          id: blind_stepper
          target: !lambda |-
            if (id(temp_position) >= id(blind_stepper).current_position) {
              return id(temp_position) + id(open_position_offset);
            } else {
              return id(temp_position);
            }
      - while:
          condition:
            lambda: |-
              return id(blind_stepper).current_position != id(temp_position) && id(blind_stepper).current_position != (id(temp_position) + id(open_position_offset));
          then:
            - cover.template.publish:
                id: kitchen_blind
                current_operation: !lambda |-
                    if(id(temp_position) >= id(blind_stepper).current_position) {
                      return COVER_OPERATION_OPENING;
                    } else {
                      return COVER_OPERATION_CLOSING;
                    }
                position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position))));'
            - delay: 1000 ms
      - cover.template.publish:
          id: kitchen_blind
          current_operation: IDLE
          position: !lambda 'return (float(float(id(blind_stepper).current_position) / float(id(open_position))));'
      - stepper.report_position:
          id: blind_stepper
          position: !lambda "return id(temp_position);"
      - stepper.set_target:
          id: blind_stepper
          target: !lambda "return id(temp_position);"
      - globals.set:
          id: current_position
          value: !lambda "return id(temp_position);"
      - output.turn_on: builtin_led
    has_position: true

I left the heating part here since it might be helpful for you guys to know about the new script feature with mode restart. The way I have this is that another Esp8266 calls this script to turn on heating, and it keeps calling it every 2 minutes or so. And in case the calls stop, after 5 minutes the script will turn off the heating, to avoid it running forever

4 Likes

Hello,
I am looking for ideas to automate these kind of curtains. Video. Any tips will be helpful. Thanks.

Wonderful to have report position and the open offset as some times my motors have torque issue.

BIG THANK’S

I’ve got a 12v worm motor with encoder. Does anyone also use these and got a proper config? It isn’t as straight forward as a stepping-motor. I’ve tried setting it up using the on_value of the encoder, but that’s hard since it isn’t able to stop at exactly 0. Every rotation of the encoder gives 11 pulses, and I believe it’s a 1 to 64 gear ratio, so that’s a lot. I’m still experimenting myself, but it would save a lot of time if somebody has an example.

So I’ve got a working config now. Just one thing I can’t get fixed is the position-report. From my understanding 0 means opened (rollerblinds up), and 1 means closed (rollerblinds down). However, when I set the position to 0 using esphome, Home Assistant puts the slider all the way to the right. This also happens when I set the state as OPEN. If i set the closed position to 0.01 and the opened to 0.99 it works almost perfectly, but I think there is something wrong in my configuration.

Here is my code so far. Since the encoder goes too fast i’m not able to make it stop exactly on 0, so that’s something to keep in mind:

globals:
   - id: blind_06_max
     type: int
     restore_value: no
     initial_value: '10000'
    
sensor:
  - platform: rotary_encoder
    name: "Rotary Encoder"
    pin_a: GPIO23
    pin_b: GPIO5
    id: blind_06_steps
    
            
switch:
  - platform: gpio
    pin: GPIO18
    name: "Blind 6 S1"
    id: "blind_06_s1"
    interlock: [blind_06_s2]
  - platform: gpio
    pin: GPIO19
    name: "Blind 6 S2"
    id: "blind_06_s2"
    interlock: [blind_06_s1]
    
cover:
  - platform: template
    name: "Blind 6"
    id: blind_06_cover
    device_class: shade
    has_position: true

    open_action:
      - switch.turn_off: blind_06_s1
      - switch.turn_on: blind_06_s2
      - cover.template.publish:
          id: blind_06_cover
          current_operation: OPENING
          position: !lambda |-
            float pos = id(blind_06_steps).state / id(blind_06_max);
            return pos < 0 ? 0 : pos;
      - wait_until: 
          lambda: 'return id(blind_06_steps).state <= 0;'
      - switch.turn_off: blind_06_s2
      - cover.template.publish:
          id: blind_06_cover
          state: OPEN
          current_operation: IDLE

    close_action:
      - switch.turn_on: blind_06_s1
      - switch.turn_off: blind_06_s2
      - cover.template.publish:
          id: blind_06_cover
          current_operation: CLOSING
          position: !lambda |-
            float pos = id(blind_06_steps).state / id(blind_06_max);
            return pos > 1 ? 1 : pos;
              
      - wait_until:
          lambda: 'return id(blind_06_steps).state >= id(blind_06_max);'
      - switch.turn_off: blind_06_s1
      - cover.template.publish:
          id: blind_06_cover
          state: CLOSED
          current_operation: IDLE
          
    stop_action:
      then:
        - switch.turn_off: blind_06_s1
        - switch.turn_off: blind_06_s2
        - cover.template.publish:
            id: blind_06_cover
            position: !lambda 'return id(blind_06_steps).state / id(blind_06_max);' 
            current_operation: IDLE

Your post as well as FF-Fox’s post along with this whole thread has been insanely helpful in me putting together the EspHome brains of my DIY smart blinds.

I’ve now got a 5V 28BYJ-48 stepper motor (bi-polar modified) directly driving a small Ikea Tupplur test blind exactly the way I want it to. This involves two buttons (Open & Close) as well as a magnetic reed sensor endstop.

  • on boot, the blind will automatically home (full open) and then return to whatever position it was at before the homing operation.
  • Up (open) button sends the blind all the way open (home)
  • Down (close) button sends the blind all the way to stepper_steps
  • Pushing either the Up or Down buttons during a move will immediately stop the blind
  • Calibration Mode: With the blind at full open, press and hold the Down (close) button for 10s. The blind will start going Down (close) until a button is pressed. The stepper_steps is now set to wherever you pressed the button just now.
  • Every time the blind goes to full open, the endstop resets the stepper’s 0 position.

Here’s my wiring:

And Here’s my code:

esphome:
  name: shade01
  platform: ESP8266
  board: nodemcuv2
  esp8266_restore_from_flash: true
  on_boot:
    then:
      - if:
          condition:
#           shade thinks it's at 0 but endstop is not triggered
            lambda: "if ((id(stepper_position) == 0) && (id(endstop).state == 0)) { return 1; } else { return 0; }"
          then:
#             open to blind to calibrate the endstop, remember the steps to do so, put the blind back to where it was
            - globals.set:
                id: stepper_state
                value: '1'
#             set the assumed stepper position to stepper_steps + 4000 and have it travel to 0
            - stepper.report_position:
                id: my_stepper
                position: !lambda "return id(stepper_steps) + 4000;"
            - stepper.set_target:
                id: my_stepper
                target: 0
#             wait until the stepper is no longer moving
            - wait_until:
                - lambda: "if (id(my_stepper).current_position == id(my_stepper).target_position) { return 1; } else { return 0; }"
#             now move the blind back to however many steps it took to reach the endstop
            - stepper.set_target:
                id: my_stepper
                target: !lambda "return id(stepper_steps) + 4000 - id(stepper_prevpos);"
            - globals.set:
                id: stepper_state
                value: '1'
            - delay: !lambda "return ((id(my_stepper).target_position - 834) * 2) + 3334;"
            - globals.set:
                id: stepper_position
                value: !lambda "return id(my_stepper).target_position;"
            - globals.set:
                id: stepper_state
                value: '0'

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
 
  ap:
    ssid: !secret ap_ssid
    password: !secret ap_password

captive_portal:

web_server:
  port: 80

mqtt:
  broker: !secret mqtt_broker
  topic_prefix: SHADE01

# Enable logging
logger:

ota:

globals:
  - id: stepper_steps
    type: int
    initial_value: '0'
    restore_value: true
  - id: stepper_prevpos
    type: float
    initial_value: '0'
  - id: stepper_position
    type: float
    initial_value: '0'
  - id: stepper_state
    type: int
    initial_value: '0'
  - id: calib_state
    type: int
    initial_value: '0'

sensor:
  - platform: uptime
    name: "Node uptime"

switch:
  - platform: restart
    name: "Restart"

binary_sensor:
  - platform: gpio
    pin:
      number: D5
      mode: INPUT_PULLUP
    name: "Open"
    filters:
        - invert:
        - delayed_on: 10ms
    on_click:
    - min_length: 50ms
      max_length: 1000ms
      then:
        - lambda: |
            if ((id(stepper_state) == 0) && (id(endstop).state == 0)) {
              //shade is stopped and not already open at endstop
              id(blind).open();
            } else {
              //shade is moving
              id(blind).stop();
            }

  - platform: gpio
    pin:
      number: D6
      mode: INPUT_PULLUP
    name: "Close"
    filters:
        - invert:
        - delayed_on: 10ms
    on_click:
    - min_length: 50ms
      max_length: 1000ms
      then:
        - lambda: |
            if ((id(stepper_state) == 0) && (id(stepper_position) < id(stepper_steps))) {
              //shade is stopped and not already closed
              id(blind).close();
            } else {
              //shade is moving
              id(blind).stop();
            }
#     a 10s press on the down button will enter calibration mode
#     shutter goes all the way up and then travels down until a button is pressed
#     where shutter is stopped is the new stepper_steps value
    - min_length: 5000ms
      max_length: 15000ms
      then:
        - lambda: |
            id(calib_state) = 1;
            if ((id(stepper_state) == 0) && (id(endstop).state == 0)) {
              //shade is stopped and not already open at endstop
              id(blind).open();
            } else if ((id(stepper_state) == 0) && (id(endstop).state == 1)) {
              //shade is stopped and already open at endstop
              id(blind).close();
            }

  - platform: gpio
    pin:
      number: D7
      mode: INPUT_PULLUP
    name: "Endstop"
    id: endstop
    filters:
        - invert:
    on_press:
      then:
#         hit the endstop, store the current steps value and reset stepper position to 0
        - globals.set:
            id: stepper_prevpos
            value: !lambda "return id(my_stepper).current_position;"
        - stepper.report_position:
            id: my_stepper
            position: 0
#         just in case, also set the target to 0
        - stepper.set_target:
            id: my_stepper
            target: 0
        - lambda: |
            if (id(stepper_state) == 1) {
              //shade is moving
              id(blind).stop();
            }

cover:
  - platform: template
    name: "Blind"
    id: blind
    open_action:
      - globals.set:
          id: stepper_state
          value: '1'
#       overshoot past the endstop to make sure it's hit (endstop will stop blind)
      - stepper.set_target:
          id: my_stepper
          target: -4000
#       wait until the stepper is no longer moving
      - wait_until:
          - lambda: "if (id(my_stepper).current_position == id(my_stepper).target_position) { return 1; } else { return 0; }"
      - globals.set:
          id: stepper_position
          value: !lambda "return id(my_stepper).target_position;"
      - globals.set:
          id: stepper_state
          value: '0'
      - if:
          condition:
#           reached past the 'top' but still haven't triggered the endstop yet
            lambda: "if(id(stepper_position) == -4000 && id(endstop).state == 0) { return 1; } else { return 0; }"
          then:
#           reset stepper position to 0
          - stepper.report_position:
              id: my_stepper
              position: 0
#           just in case, also set the target to 0
          - stepper.set_target:
              id: my_stepper
              target: 0
    close_action:
      - globals.set:
          id: stepper_state
          value: '1'
      - stepper.set_target:
          id: my_stepper
#         for calibration the number of steps is doubled
          target: !lambda "if(id(calib_state) == 0) { return id(stepper_steps); } else { return id(stepper_steps) * 2; }"
#       wait until the stepper is no longer moving
      - wait_until:
          - lambda: "if (id(my_stepper).current_position == id(my_stepper).target_position) { return 1; } else { return 0; }"
      - globals.set:
          id: stepper_position
          value: !lambda "return id(my_stepper).target_position;"
      - globals.set:
          id: stepper_state
          value: '0'
      - lambda: |
          if (id(calib_state) == 1) {
            //reached double the current stepper distance
            id(stepper_steps) = id(stepper_position);
            //continue with calibration
            id(blind).close();
          }
    stop_action:
      - stepper.set_target:
          id: my_stepper
          target: !lambda "return id(my_stepper).current_position;" 
      - globals.set:
          id: stepper_position
          value: !lambda "return id(my_stepper).current_position;"
      - globals.set:
          id: stepper_state
          value: '0'
      - lambda: |
          if (id(calib_state) == 1 && id(endstop).state == 0) {
            //stopping at something other than the endstop
            id(calib_state) = 0;
            id(stepper_steps) = id(stepper_position);
          }
      - cover.template.publish:
          id: blind
          position: !lambda "return 1 - (id(stepper_position) / id(stepper_steps));"
    position_action:
      - stepper.set_target:
          id: my_stepper
          target: !lambda "return id(stepper_steps) * (1 - pos);"
    optimistic: true
    assumed_state: true
    has_position: true
    
stepper:
  - platform: a4988
    id: my_stepper
    dir_pin: D3
    step_pin: D2
    max_speed: 500 steps/s

    # Optional:
    sleep_pin: D1
    acceleration: 300
    deceleration: 300

3 Likes

@ashwin I have implemented your code and it works well, however I want to invert the controls as currently when booted the blind thinks it is at the top of the movement. Essentially I want 0 to be at the bottom. I have tried to modify your code but to no avail.

Any help would be much appreciated

@HumanSkunk Would changing the motor wiring do the job?

I modified the opening and closing loops to do the opposite but kept the logic the same. It seems to work. So now defaults to the bottom. I guess inverting the outputs to the motor may have done the same thing.

Now I need to work out how to 0 the blind with a switch.

Once your endstop switch is triggered, that’s the queue to tell the stepper that this point is 0 (stepper.report_position) and to also set this point as the current target for the stepper … so it doesn’t move past it (stepper.set_target).

Thanks, but I am not using a stepper motor . I tried to get it working but it just didnt have enough torque to lift my heavy blind. Instead I have DC motor with a wormgear.

I have managed to get it working with something similar by a switch and setting the rotary encoder position to zero.

Now 3D printing cases for it all so I can hopefully mount it all up next weekend.

Ah, I understand now. Looking forward to pictures?

I’m sacrificing speed for torque by still using 28BYJ-48 5V steppers with a print in place 2.5:1 planetary gearing on the output to handle my larger blinds. Fingers crossed that it works and I’m able to live with the (lack of) speed.

1 Like

Nice. My roller blind requires nearly 2.5kg of force to lift it, the magnets in the stepper just kept skipping. The wormier takes about 15 to 20 seconds to fully open which for a bedroom blind is fine. I plan to have home assistant automate it anyways so it isn’t like I will be waiting for it to happen.

I have dropped in some pictures of what I have so far. Unsure about heat so I may have to drill some holes into the case. Essentially it will pull round the chain as it rotates. Clearance issues made mounting it at the top impossible without modifying the blind which I didnt want to do.

I only recently got a 3D printer so I am basically finding things I can make with it.

!

Very cool!

How is that going to be attached to the wall/floor?

Since your motor won’t run for more than 15-20 seconds at a time, you probably won’t have to worry about adding ventilation to your enclosure.

P.S. - If you’re looking to impress the significant other with your 3D printer, my wife recent approved of my laundry detergent cup holder print (similar to this).

I am currently printing a bracket that will fix to the wall with a slider for a set of screws so I can set the right tension. The box will then slot into the holder and be held in with some 3D printed dowels. Well that’s the plan anyways.

I do now have a stepper and motor driver that I dont know what to do with…

1 Like