Motor on a roller blind - ESPHome version?

Hi all,
I come in this topic because I see a lot of specialist of Esphome related with stepper motor. My goal is to buiild a 1-axis solar tracker via i) a magnetic compass, here a BMM150 and ii) a stepper motor with the a4988. In my idea, the control of the stepper motor (direction & step) must be done on the difference between the heading given by the compass and the solar heading given the sun object with my local GPS coordinates. So in theory it’s simple if the difference is +D then set the target of the stepper with int(D/delta_angle), if the difference is -D then set with -int(D/delta_angle).
Here my current code

esphome:
  name: suiveur_1axe
  platform: ESP8266
  board: nodemcuv2
  includes:
    - BMM150_custom_sensor.h

wifi:
  ssid: !secret esphome_ssid
  password: !secret esphome_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Suiveur 1Axe"
    password: !secret esphome_password

captive_portal:

# Enable logging
logger:

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

ota:

sun:
  latitude: !secret esphome_lat
  longitude: !secret esphome_long 
  
time:
  - platform:  homeassistant    #sntp
    id: homeassistant_time  

output:
  - platform: gpio
    pin:
      number: GPIO02
      inverted: True
    id: gpio_02
    
light:
  - platform: binary   
    output: gpio_02
    name: suiveur_1axe_led_status
    id: suiveur_1axe_led_status

i2c:
  sda: GPIO12
  scl: GPIO14
  id: bus_i2c

sensor:

  - platform: sun
    name: suiveur_1axe_elevation_soleil
    type: elevation

  
  - platform: sun
    name: suiveur_1axe_azimuth_soleil
    id: suiveur_1axe_azimuth_soleil
    type: azimuth
    update_interval: 1s

#  - platform: qmc5883l
#    address: 0x0D
#    field_strength_x:
#      name: "suiveur_1axe_X"
#      id: suiveur_1axe_X
#    field_strength_y:
#      name: "suiveur_1axe_Y"
#      id: suiveur_1axe_Y
#    field_strength_z:
#      name: "suiveur_1axe_Z"
#      id: suiveur_1axe_Z
#    heading:
#      name: "suiveur_1axe_azimuth_plateau"
#      id: suiveur_1axe_azimuth_plateau
#    range: 200uT
#    oversampling: 256x
#    update_interval: 1s

  - platform: custom
    id: bmm150_id
    lambda: |-
      auto BMM150 = new BMM150CustomSensor();
      App.register_component(BMM150);
      return {BMM150->heading_sensor};

    sensors:
    - name: "suiveur_1axe_azimuth_plateau"
      id: suiveur_1axe_azimuth_plateau
      unit_of_measurement: "°"
      accuracy_decimals: 1
      icon: "mdi:compass-outline"
      filters:
       - sliding_window_moving_average:
           window_size: 3
           send_every: 1
      
#      filters:
#      - sliding_window_moving_average:
#          window_size: 15
#          send_every: 1  
    
#    - name: "suiveur_1axe_x"
#      unit_of_measurement: "uT"
#      accuracy_decimals: 2
#      icon: "mdi:magnet"
#      filters:
#      - sliding_window_moving_average:
#          window_size: 15
#          send_every: 1
#    - name: "suiveur_1axe_y"
#      unit_of_measurement: "uT"
#      accuracy_decimals: 2
#      icon: "mdi:magnet"
#      filters:
#      - sliding_window_moving_average:
#          window_size: 15
#          send_every: 1
          
#    - name: "suiveur_1axe_z"
#      unit_of_measurement: "uT"
#      accuracy_decimals: 2
#      icon: "mdi:magnet"  
#      filters:
      #- median:
      #    window_size: 7
      #    send_every: 1
#      - sliding_window_moving_average:
#          window_size: 15
#          send_every: 1  
    
    
#  - platform: template
#    id: suiveur_1axe_azimuth_plateau_smoothed
#    name: suiveur_1axe_azimuth_plateau_smoothed
#    update_interval: 1s
#    unit_of_measurement: "°"
#    lambda: return id(suiveur_1axe_azimuth_plateau).state + 0*180.0;
#    filters:
#      - sliding_window_moving_average:
#          window_size: 5
#          send_every: 1  
    
  - platform: template
    id: suiveur_1axe_azimuth_difference
    name: suiveur_1axe_azimuth_difference
    update_interval: 1s
    unit_of_measurement: "°"
    lambda: return ( id(suiveur_1axe_azimuth_soleil).state - id(suiveur_1axe_azimuth_plateau).state);
    filters:
      - sliding_window_moving_average:
          window_size: 5
          send_every: 1
    on_value:
      - if:
          condition:
            and:
             - lambda: |-
                 return (id(stepper_suiveur_1axe).current_position == id(stepper_suiveur_1axe).target_position );
             -  lambda: |-
                 return (  (  ( float(id(suiveur_1axe_azimuth_difference).state)  < -2.0) | ( float(id(suiveur_1axe_azimuth_difference).state)  > 2.0) )     );
             
            #not:   
            # - sensor.in_range:
            #     id: suiveur_1axe_azimuth_difference
            #     below: -2.0
            #     above: 2.0
               
            #lambda: |-
            #  return ( fabs(float(id(suiveur_1axe_azimuth_difference).state))  > 1.0 );
          then: 
            #- delay: 30s
            - light.turn_on:
                id: suiveur_1axe_led_status
            - stepper.set_target:
               id: stepper_suiveur_1axe
               target: !lambda |-
                if (float(id(suiveur_1axe_azimuth_difference).state) > 10.0) 
                {
                  return -int( float(id( suiveur_1axe_azimuth_difference).state)/((1.8/20.0)) );
                } 
                else if (float(id(suiveur_1axe_azimuth_difference).state) >  2.0)
                {
                   return -int( float(id( suiveur_1axe_azimuth_difference).state)/((1.8/10.0)) );
                }
                else if (float(id(suiveur_1axe_azimuth_difference).state) < -2.0)
                {
                  return int( float(id( suiveur_1axe_azimuth_difference).state)/((1.8/10.0)) );
                }
                else if (float(id(suiveur_1axe_azimuth_difference).state) < -10.0)
                {
                  return int( float(id( suiveur_1axe_azimuth_difference).state)/((1.8/20.0)) );
                }
            #- delay: 10s
            #- while:
            #    condition:
            #      lambda: 'return id(stepper_suiveur_1axe).current_position != id(stepper_suiveur_1axe).target_position;'
            #    then:
            #      - logger.log: "Rotation"
                
            - light.turn_off:
                id: suiveur_1axe_led_status      
          
  
       #- if:
          #condition:
          # Should return either true or false
          #  lambda: |-
          #    return (  (  ( float(id(suiveur_1axe_azimuth_difference).state)  < -1.0) | ( float(id(suiveur_1axe_azimuth_difference).state)  > 1.0) ) & (id(stepper_suiveur_1axe).current_position == id(stepper_suiveur_1axe).target_position )       );
          #              # return ( ( fabs(float(id(suiveur_1axe_azimuth_difference).state) ) > 1.0) & (id(stepper_suiveur_1axe).current_position == id(stepper_suiveur_1axe).target_position )       );

        #  then:
        #  - stepper.set_target:
        #      id: stepper_suiveur_1axe
        #      target: !lambda |-
        #        if (float(id(suiveur_1axe_azimuth_difference).state) > 0.0) 
        #        {
        #          return -10;
        #        } else 
        #        {
        #          return 10;
        #        }
              #!lambda  return ( -sign(float(id( suiveur_1axe_azimuth_difference).state)) ) ;
              #target: !lambda  return ( -int(float(id( suiveur_1axe_azimuth_difference).state)/((1.8/10))) ) ;
            
  - platform: uptime
    #name: "up_suiveur_1axe"
    id: uptime_sec  
    
  - platform: wifi_signal
    name: "WiFi puissance_suiveur_1axe"
    update_interval: 10s      

switch:
  - platform: restart
    name: "restart_suiveur_1axe"

stepper:
  - platform: a4988
    id: stepper_suiveur_1axe
    
    step_pin: 
      number: D0
      inverted: False
      
    dir_pin:
      number: D1
      inverted: False
      
    max_speed: 60 steps/s

    # Optional:
    sleep_pin:
      number: D2
      inverted: False
      
    acceleration: inf
    deceleration: inf
    
    
cover:
  - platform: template
    name: "suiveur_1axe"
    id: suiveur_1axe
    open_action:
      - stepper.set_target:
          id: stepper_suiveur_1axe
          target: -1000

    close_action:
      - stepper.set_target:
          id: stepper_suiveur_1axe
          target: 1000
    
    stop_action:
      - stepper.set_target:
          id: stepper_suiveur_1axe
          target: !lambda return id(stepper_suiveur_1axe).current_position;      
    
    optimistic: true    
    
    
binary_sensor:
  - platform: status
    name: "suiveur_1axe_status" 
    
    
text_sensor:
  - platform: template
    name: suiveur_1axe__uptime
    lambda: |-
      int seconds = (id(uptime_sec).state);
      int days = seconds / (24 * 3600);
      seconds = seconds % (24 * 3600); 
      int hours = seconds / 3600;
      seconds = seconds % 3600;
      int minutes = seconds /  60;
      seconds = seconds % 60;
      return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() };
    icon: mdi:clock-start
    update_interval: 113s    
    
  - platform: version
    name: "suiveur_1axe__ESPHome_version"  

In practice it’s not working … oscillating between the 0 value or even worser can converge the a non-zero value :frowning: I think the problem comes from the fact that during the rotation of the motor, the template sensor associated with difference of heading is also changing and then a new set_target is set.

When I would like to write and I don’t know yet how is to
i) compute the difference of heading and be sure its value is stablized then if the motor is in IDLE
ii) send the command to rotate correspondly to this difference AND do not update the difference sensor and/or wait the rotation reaches the target_position in order to send a new rotation command.

I hope I was more or less clear…

I’m trying to build my own roller but I find the 28BYJ-48 stepper motor (in 12v) has not enough torque to move my blind (36’’ or 91 cm wide). What kind of motor do you use?

Mine are 91x190 and it’s fine. Slow but fine. What power supply do you use? Is your motor trying to do anything at all? I.e. buzzing at least?

I use nema 17 motors

I use a 12v 2.0A PSU. Motor is working but it skip steps as soon as torque is needed. I guess my blinds are a bit too heavy.

That will be my next try. Is it okay with uln2003 ?

I initially used 12v 0.25A power supply and it worked. So you have plenty of power out there. I use uln2003 so it’s fine. If your motor works without the blind as expected then I guess blind is too heavy.

Is use a driver that was left over from my 3d printer project

would you been kind enough to explain how you did it? I mean what driver how you using and how do you pilot it from esphome?

I should have search first: using a A4988 and a control board, I guess.

Great project! Printed the holder anf got it up amd running in no time.
One question, what is the best way to deal with the target being set to 0 on boot? Do you store it in hass ot flash?

@fondberg I wrote THIS for the esphome on a motor blind, set it up using the WebUI? or a single button on the roller blind. You just set it up once and you’re good. I have them running on 4 blinds at home without issue.

1 Like

I see, so in case of power being cut you calibrate them again?

The position is saved, it should be fine after power is cut.

Understand. I thought the docs said position is always 0 at boot and the report position was used to set it to something else.

I approached this a bit differently than @RoadkillUK because I believe the ESP only needs to perform basic actions as much as possible and so I tried to move all sophisticated logic to HA. But I also use global variables to store current position of the steppers and save them in flash for cases of power cut. 100,000 flash writes available on ESP is not too bad :slight_smile:

I did my first roller but didn’t add a button. I will make the second with a button and try the version @RoadkillUK has made

There’s no need for a button, if you go to the IP of the blind, you will see something like the image below, use these buttons and you don’t need a physical button.

Alright!
Can ypu quickly describe what the button does?

EDITED

This should cover it.

This sketch will add 2 switches named <upper_devicename> Setup Switch and Setup Button Use your mobile or tablet and connect to http://<BlindIP> to set up the blind

  • Turn on the Setup Switch to enter setup mode
  • Press Setup button to start the blind closing
  • Press Setup button again when closed and blind starts to open
  • Press Setup button again when blind is fully open
  • Job Done