# Shading

Process

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

IDAbbrevNameTypeDefaultDescription
toggleTToggleBOOLEANfalseSwitches between open, stop, and close states using a single button control
part_openPOPartial openBOOLEANfalseActivates opening when the button is pressed and held
part_closePCPartial closeBOOLEANfalseActivates closing when the button is pressed and held
full_openFOFull openBOOLEANfalseFully opens the shading device; stops movement if already in motion
full_closeFCFull closeBOOLEANfalseFully closes the shading device; stops movement if already in motion
slightly_openSOSlightly openBOOLEANfalseVenetian 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_alarmWAWind alarmBOOLEANfalseMoves shading to a safe position during high wind conditions and locks the controls
dwcDWCDoor window contactBOOLEANfalseAutomatically opens the shading device when a door or window is detected open
offOFFOffBOOLEANfalseStops shading device movement when active and blocks other operations
positionPPositionNUMBER0Moves the shading device to the specified position as a percentage (0=fully closed, 100=fully open)
slatsSSlatsNUMBER0Moves the slats of venetian blinds to the specified angle given as a percentage (0=vertical, 50=horizontal, 100=closed)

# Outputs

IDAbbrevNameTypeDefaultDescription
openOOpenBOOLEANfalseActivates when the shading device should be opening.
closeCCloseBOOLEANfalseActivates when the shading device should be closing.
positionPPositionNUMBER0Represents the current position of the shading device as a percentage (0=fully closed, 100=fully open)
slatsSSlats positionNUMBER0Represents the current angle of slats as a percentage (0=vertical, 50=horizontal, 100=closed)

# Configuration

IDNameTypeDefaultUnitDescription
shading_typeShading typeENUM0Defines 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_durationOpening durationNUMBER80sThe time taken to fully open the shading device, in seconds

Details:

≥ 0
closing_durationClosing durationNUMBER80sThe time taken to fully close the shading device, in seconds

Details:

≥ 0
return_durationReturn durationNUMBER0.8sReturn 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_positionWind alarm positionNUMBER0%The position of the shading device during wind alarm activation, as a percentage of fully open (0-100)

Details:

≥ 0
≤ 100
motor_lock_durationMotor lock durationNUMBER0.5sTime required to release the motor lock between direction changes, in seconds

Details:

≥ 0

# State

IDNameTypeDefaultUnitDescription
positionPositionNUMBER0.0%Raw value of the output position as a percentage (0-100), not rounded
desired_positionDesired positionNUMBER0.0%Desired position. Used only to pass values between callback executions
slatsSlats positionNUMBER0.0%Raw value of the slats position as a percentage (0-100), not rounded
desired_slatsDesired slats positionNUMBER0.0%Desired slats position. Used only to pass values between callback executions
steps_leftSteps leftNUMBER0stepsNumber of steps remaining for completing the current movement
steps_left_slatsSteps left for slatsNUMBER0stepsNumber of steps remaining for completing the current movement for slats
was_openingWas openingBOOLEANfalseIndicates whether the previous movement was in the opening direction
last_movement_tsLast movement timestampNUMBER-60000millisecondsTimestamp 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
}
Controls shading devices like blinds, curtains, and awnings