# Shading
Controls shading devices like blinds, curtains, and awnings
Shading
T
PO
PC
FO
FC
SO
WA
DWC
OFF
P
S
O
C
P
S
# Inputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
toggle | T | Toggle | BOOLEAN | false | Switches between open, stop, and close states using a single button control |
part_open | PO | Partial open | BOOLEAN | false | Activates opening when the button is pressed and held |
part_close | PC | Partial close | BOOLEAN | false | Activates closing when the button is pressed and held |
full_open | FO | Full open | BOOLEAN | false | Fully opens the shading device; stops movement if already in motion |
full_close | FC | Full close | BOOLEAN | false | Fully closes the shading device; stops movement if already in motion |
slightly_open | SO | Slightly open | BOOLEAN | false | Venetian blinds close completely and move slats to the horizontal position based on the configured 'slightly open duration'. Roller blinds, curtains, and awnings move to the slightly open position according to the same parameter. |
wind_alarm | WA | Wind alarm | BOOLEAN | false | Moves shading to a safe position during high wind conditions and locks the controls |
dwc | DWC | Door window contact | BOOLEAN | false | Automatically opens the shading device when a door or window is detected open |
off | OFF | Off | BOOLEAN | false | Stops shading device movement when active and blocks other operations |
position | P | Position | NUMBER | 0 | Moves the shading device to the specified position as a percentage (0=fully closed, 100=fully open) |
slats | S | Slats | NUMBER | 0 | Moves the slats of venetian blinds to the specified angle given as a percentage (0=vertical, 50=horizontal, 100=closed) |
# Outputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
open | O | Open | BOOLEAN | false | Activates when the shading device should be opening. |
close | C | Close | BOOLEAN | false | Activates when the shading device should be closing. |
position | P | Position | NUMBER | 0 | Represents the current position of the shading device as a percentage (0=fully closed, 100=fully open) |
slats | S | Slats position | NUMBER | 0 | Represents the current angle of slats as a percentage (0=vertical, 50=horizontal, 100=closed) |
# Configuration
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
shading_type | Shading type | ENUM | 0 | Defines the type of shading device: Venetian blinds, Roller blinds, Curtains (both sides), Curtains (left), Curtains (right), Awning Details: Values: Venetian blinds, Roller blinds, Curtains (both sides), Curtains (left), Curtains (right), Awning | |
opening_duration | Opening duration | NUMBER | 80 | s | The time taken to fully open the shading device, in seconds Details: ≥ 0 |
closing_duration | Closing duration | NUMBER | 80 | s | The time taken to fully close the shading device, in seconds Details: ≥ 0 |
return_duration | Return duration | NUMBER | 0.8 | s | Return duration to position the shading device slightly open from fully open position. For venetian blinds, duration for horizontal alignment of slats. Details: ≥ 0 |
wind_alarm_position | Wind alarm position | NUMBER | 0 | % | The position of the shading device during wind alarm activation, as a percentage of fully open (0-100) Details: ≥ 0 ≤ 100 |
motor_lock_duration | Motor lock duration | NUMBER | 0.5 | s | Time required to release the motor lock between direction changes, in seconds Details: ≥ 0 |
# State
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
position | Position | NUMBER | 0.0 | % | Raw value of the output position as a percentage (0-100), not rounded |
desired_position | Desired position | NUMBER | 0.0 | % | Desired position. Used only to pass values between callback executions |
slats | Slats position | NUMBER | 0.0 | % | Raw value of the slats position as a percentage (0-100), not rounded |
desired_slats | Desired slats position | NUMBER | 0.0 | % | Desired slats position. Used only to pass values between callback executions |
steps_left | Steps left | NUMBER | 0 | steps | Number of steps remaining for completing the current movement |
steps_left_slats | Steps left for slats | NUMBER | 0 | steps | Number of steps remaining for completing the current movement for slats |
was_opening | Was opening | BOOLEAN | false | Indicates whether the previous movement was in the opening direction | |
last_movement_ts | Last movement timestamp | NUMBER | -60000 | milliseconds | Timestamp of the last movement of the shading device used to add a motor lock pause on direction change |
# Source Code
View Volang source
callback_ms = 100
callback_ms_real = 99 // leave a bit room for callback scheduling to get precise 100ms
channel = input::channel()
value = input::value()
shading_type = config::get("shading_type")
fn get_motor_lock_ms() {
return math::round(config::get("motor_lock_duration") * 1000)
}
fn is_moving() {
return output::get("open") or output::get("close")
}
fn mark_moving() {
if (is_moving()) {
state::set("last_movement_ts", time::uptime())
}
}
mark_moving()
fn open_close(open) {
callback::clear()
output::set("open", open)
output::set("close", !open)
state::set("was_opening", open)
}
fn open() {
open_close(true)
}
fn close() {
open_close(false)
}
fn stop() {
callback::clear()
output::set("open", false)
output::set("close", false)
}
// slats exists only for venetian blinds, so ensure they are zeroed for other configurations
if (shading_type != 0 and state::get("slats") != 0) {
state::set("slats", 0)
}
if (state::get("slats") >= 100 and state::get("position") <= 0) {
// 0 and 100 position of slats are equal on position == 0
state::set("slats", 0)
}
fn calculate_position_diff(position_in) {
// position_diff - percentage:
// zero - ok
// negative - need to move in closing direction
// positive - need to move in opening direction
position_diff = -100
if (position_in != 0) {
position_diff = position_in - state::get("position")
}
return position_diff
}
fn calculate_slats_diff(position_diff, position_in, slats_in) {
slats_position_diff = 0
if ((slats_in == 0 or slats_in == 100) and position_in == 0) {
// no-op
} else if (position_diff == 0) {
slats_position_diff = slats_in - state::get("slats")
} else if (position_diff > 0) { // opening, so slats will close (0)
slats_position_diff = slats_in
} else { // closing, so slats will open (100)
slats_position_diff = slats_in - 100
}
return slats_position_diff
}
fn calculate_is_next_move_towards_open(position_diff, slats_position_diff) {
move_towards_open = false
if (position_diff == 0) { // slats only
if (slats_position_diff > 0) { // closing direction to start opening slats
move_towards_open = false
} else { // opening direction to start closing slats
move_towards_open = true
}
} else if (position_diff < 0) { // closing
move_towards_open = false
} else { // opening
move_towards_open = true
}
return move_towards_open
}
extern fn onCallback(shading_type, callback_ms, callback_ms_real, value) {
steps_left = state::get("steps_left")
steps_left -= 1
if (steps_left <= 0) {
steps_left = 0
}
state::set("steps_left", steps_left)
last_slats = state::get("slats")
current_slats = last_slats
last_position = state::get("position")
current_position = last_position
if (value == 1 or value == 3) { // opening callback - 1: open, 3: open & close
if (shading_type == 0) { // slats exists only for venetian blinds
if (config::get("return_duration") == 0) { // handle division by zero
current_slats = 0
last_slats = current_slats
} else {
current_slats = last_slats - ((100 * callback_ms) / (2 * config::get("return_duration") * 1000.0))
}
if (current_slats <= 0) {
current_slats = 0
}
}
if (math::round(last_slats) <= 0) { // slats fully moved, start changing position
current_position = last_position + ((100 * callback_ms) / (config::get("opening_duration") * 1000.0))
if (current_position >= 100) {
current_position = 100
}
}
state::set("slats", current_slats)
output::set("slats", math::round(current_slats))
state::set("position", current_position)
output::set("position", math::round(current_position))
if (steps_left > 0) {
callback::set(callback_ms_real, "onCallback", shading_type, callback_ms, callback_ms_real, value)
} else if (value == 1 or (value == 3 and shading_type != 0)) {
stop()
} else if (value == 3) {
stop()
steps_left_slats = state::get("steps_left_slats")
if (steps_left_slats == 0) {
return
}
callback::set(0, "onCallback", shading_type, callback_ms, callback_ms_real, 10)
}
return
}
if (value == 2 or value == 4) { // closing callback - 2: close, 4: close + open
if (shading_type == 0) { // slats exists only for venetian blinds
if (config::get("return_duration") == 0) { // handle division by zero
current_slats = 100
last_slats = current_slats
} else {
if (last_slats <= 0 and last_position <= 0) {
// prevent a 0 -> 100 move for slats in closed blind position
current_slats = 0
} else {
current_slats = last_slats + ((100 * callback_ms) / (2 * config::get("return_duration") * 1000.0))
}
}
if (current_slats >= 100) {
current_slats = 100
}
if (last_slats == 100) { // slats fully moved, start changing position
current_position = last_position - ((100 * callback_ms) / (config::get("closing_duration") * 1000.0))
if (current_position <= 0) {
current_position = 0
}
}
} else { // other shading types
current_position = last_position - ((100 * callback_ms) / (config::get("closing_duration") * 1000.0))
if (current_position <= 0) {
current_position = 0
}
}
state::set("slats", current_slats)
// 0 and 100 positions are the same when fully closed
if (math::round(current_position) == 0 and (math::round(current_slats) == 0 or math::round(current_slats) == 100)) {
output::set("slats", 0)
} else {
output::set("slats", math::round(current_slats))
}
state::set("position", current_position)
output::set("position", math::round(current_position))
if (steps_left > 0) {
callback::set(callback_ms_real, "onCallback", shading_type, callback_ms, callback_ms_real, value)
} else if (value == 2 or (value == 4 and shading_type != 0)) {
stop()
} else if (value == 4) {
stop()
if (state::get("steps_left_slats") == 0) {
return
}
callback::set(0, "onCallback", shading_type, callback_ms, callback_ms_real, 11)
}
return
}
if (value == 10 or value == 11) { // motor_lock, 10: open to close, 11: close to open
if (value == 10) {
callback::set(get_motor_lock_ms(), "onCallback", shading_type, callback_ms, callback_ms_real, 20)
} else {
callback::set(get_motor_lock_ms(), "onCallback", shading_type, callback_ms, callback_ms_real, 21)
}
return
}
if (value == 20) { // start return movement for slats alignment - open to close
close()
state::set("steps_left", state::get("steps_left_slats"))
callback::set(callback_ms_real, "onCallback", shading_type, callback_ms, callback_ms_real, 2)
return
}
if (value == 21) { // start return movement for slats alignment - close to open
open()
state::set("steps_left", state::get("steps_left_slats"))
callback::set(callback_ms_real, "onCallback", shading_type, callback_ms, callback_ms_real, 1)
return
}
return
}
fn move(position_in, slats_in, shading_type, callback_ms, callback_ms_real) {
full_opening_steps = (1000 * config::get("opening_duration")) / callback_ms
full_opening_steps_left = full_opening_steps * (100 - state::get("position")) / 100.0
full_closing_steps = (1000 * config::get("closing_duration")) / callback_ms
slats_full_rotation_steps = (1000 * 2 * config::get("return_duration")) / callback_ms
if (shading_type != 0) { // slats are present only for venetian blinds
slats_full_rotation_steps = 0
}
slats_full_rotation_steps_left_opening = slats_full_rotation_steps * state::get("slats") / 100.0
slats_full_rotation_steps_left_closing = slats_full_rotation_steps * (100 - state::get("slats")) / 100.0
steps_left_opening = math::round(full_opening_steps_left + slats_full_rotation_steps_left_opening)
steps_left_closing = math::round(full_closing_steps + slats_full_rotation_steps)
position_diff = calculate_position_diff(position_in)
slats_position_diff = calculate_slats_diff(position_diff, position_in, slats_in)
callback_id = 0
steps_left = 0
steps_left_slats = 0
if (slats_position_diff == 0) {
// no-op
} else if (slats_position_diff > 0) {
steps_left_slats = slats_full_rotation_steps * slats_position_diff / 100
} else {
steps_left_slats = slats_full_rotation_steps * -slats_position_diff / 100
}
if (position_diff == 0 and slats_position_diff == 0) {
return
}
if (position_diff == 0) { // slats only
steps_left = steps_left_slats
steps_left_slats = 0
if (slats_position_diff > 0) { // closing direction to start opening slats
close()
callback_id = 2
} else { // opening direction to start closing slats
open()
callback_id = 1
}
} else if (position_diff < 0) { // closing
close()
steps_left = full_closing_steps * -position_diff / 100 + slats_full_rotation_steps_left_closing
callback_id = 4
} else { // opening
open()
steps_left = full_opening_steps * position_diff / 100 + slats_full_rotation_steps_left_opening
callback_id = 3
}
state::set("steps_left", steps_left)
state::set("steps_left_slats", steps_left_slats)
callback::set(callback_ms_real, "onCallback", shading_type, callback_ms, callback_ms_real, callback_id)
}
extern fn moveCallback(shading_type, callback_ms, callback_ms_real) {
move(state::get("desired_position"), state::get("desired_slats"), shading_type, callback_ms, callback_ms_real)
}
fn safe_move(position_in, slats_in, shading_type, callback_ms, callback_ms_real) {
state::set("desired_position", position_in)
state::set("desired_slats", slats_in)
time_since_last_move = time::uptime() - state::get("last_movement_ts")
if (time_since_last_move >= get_motor_lock_ms()) {
callback::set(0, "moveCallback", shading_type, callback_ms, callback_ms_real)
return
}
position_diff = calculate_position_diff(position_in)
slats_position_diff = calculate_slats_diff(position_diff, position_in, slats_in)
next_move_towards_open = calculate_is_next_move_towards_open(position_diff, slats_position_diff)
if (state::get("was_opening") == next_move_towards_open) {
callback::set(0, "moveCallback", shading_type, callback_ms, callback_ms_real)
} else {
callback::set(get_motor_lock_ms(), "moveCallback", shading_type, callback_ms, callback_ms_real)
}
}
if (channel == "position" or channel == "slats") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
callback::clear()
safe_move(input::get("position"), input::get("slats"), shading_type, callback_ms, callback_ms_real)
return
}
if (channel == "part_open") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
stop()
if (value) {
safe_move(100, 0, shading_type, callback_ms, callback_ms_real)
}
return
}
if (channel == "part_close") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
stop()
if (value) {
safe_move(0, 0, shading_type, callback_ms, callback_ms_real)
}
return
}
if (channel == "dwc") {
if (input::get("wind_alarm") or input::get("off") or !value) {
return
}
// do not activate if already open
if (state::get("position") <= 0) {
return
}
safe_move(0, 0, shading_type, callback_ms, callback_ms_real)
return
}
if (channel == "wind_alarm") {
if (input::get("off") or !value) {
return
}
callback::clear()
wap = config::get("wind_alarm_position")
position_diff = wap - state::get("position")
slats = 0
if (position_diff < 0) { // closing
slats = 100 // don't move slats after reaching target position
}
safe_move(config::get("wind_alarm_position"), slats, shading_type, callback_ms, callback_ms_real)
return
}
if (channel == "off") {
if (value) { // reset on raising edge, no-op on fall
stop()
}
return
}
// pulse signals below - trigger only on rising edge
if (value == false) {
return
}
if (channel == "toggle") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
if (is_moving()) { // stop on movement
stop()
} else if (state::get("was_opening")) { // toggle to closing
safe_move(0, 100, shading_type, callback_ms, callback_ms_real)
} else { // toggle to opening
safe_move(100, 0, shading_type, callback_ms, callback_ms_real)
}
return
}
if (channel == "full_open" or channel == "slightly_open") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
if (is_moving()) { // stops if in motion
stop()
return
}
position = 100
slats = 0
if (channel == "slightly_open") {
if (shading_type == 0) {
slats = 50
} else {
position = 100 * ((config::get("opening_duration") - config::get("return_duration")) / config::get("opening_duration"))
}
}
safe_move(position, slats, shading_type, callback_ms, callback_ms_real)
return
}
if (channel == "full_close") {
if (input::get("wind_alarm") or input::get("off") or input::get("dwc")) {
return
}
if (is_moving()) { // stops if in motion
stop()
return
}
safe_move(0, 0, shading_type, callback_ms, callback_ms_real)
return
}
