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.
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
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
@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
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.
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…