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