# Shading

Przetwarzanie

Steruje urządzeniami zacieniającymi takimi jak żaluzje, zasłony i markizy

Shading
T
PO
PC
FO
FC
SO
WA
DWC
OFF
P
S
O
C
P
S

# Wejścia

IDSkrótNazwaTypDomyślnieOpis
toggleTPrzełączBOOLEANfalsePrzełącza między stanami otwierania, zatrzymania i zamykania za pomocą jednego przycisku
part_openPOCzęściowe otwarcieBOOLEANfalseAktywuje otwieranie gdy przycisk jest wciśnięty i przytrzymany
part_closePCCzęściowe zamknięcieBOOLEANfalseAktywuje zamykanie gdy przycisk jest wciśnięty i przytrzymany
full_openFOPełne otwarcieBOOLEANfalseW pełni otwiera urządzenie zacieniające; zatrzymuje ruch jeśli już w ruchu
full_closeFCPełne zamknięcieBOOLEANfalseW pełni zamyka urządzenie zacieniające; zatrzymuje ruch jeśli już w ruchu
slightly_openSOLekkie otwarcieBOOLEANfalseŻaluzje weneckie zamykają się całkowicie i przesuwają lamele do pozycji poziomej. Rolety, zasłony i markizy przesuwają się do pozycji lekkiego otwarcia.
wind_alarmWAAlarm wiatrowyBOOLEANfalsePrzesuwa zacienianie do bezpiecznej pozycji podczas silnego wiatru i blokuje sterowanie
dwcDWCKontakt drzwi/oknaBOOLEANfalseAutomatycznie otwiera urządzenie zacieniające gdy wykryto otwarte drzwi lub okno
offOFFWyłączBOOLEANfalseZatrzymuje ruch urządzenia zacieniającego gdy aktywne i blokuje inne operacje
positionPPozycjaNUMBER0Przesuwa urządzenie zacieniające do określonej pozycji jako procent (0=zamknięte, 100=otwarte)
slatsSLameleNUMBER0Przesuwa lamele żaluzji weneckich do określonego kąta jako procent (0=pionowo, 50=poziomo, 100=zamknięte)

# Wyjścia

IDSkrótNazwaTypDomyślnieOpis
openOOtwieranieBOOLEANfalseAktywuje się gdy urządzenie zacieniające powinno się otwierać
closeCZamykanieBOOLEANfalseAktywuje się gdy urządzenie zacieniające powinno się zamykać
positionPPozycjaNUMBER0Reprezentuje bieżącą pozycję urządzenia zacieniającego jako procent (0=zamknięte, 100=otwarte)
slatsSPozycja lameliNUMBER0Reprezentuje bieżący kąt lameli jako procent (0=pionowo, 50=poziomo, 100=zamknięte)

# Konfiguracja

IDNazwaTypDomyślnieJednostkaOpis
shading_typeTyp zacienianiaENUM0Określa typ urządzenia: Żaluzje weneckie, Rolety, Zasłony (obie strony), Zasłony (lewa), Zasłony (prawa), Markiza

Szczegóły:

Wartości: Venetian blinds, Roller blinds, Curtains (both sides), Curtains (left), Curtains (right), Awning
opening_durationCzas otwieraniaNUMBER80sCzas potrzebny do pełnego otwarcia urządzenia, w sekundach

Szczegóły:

≥ 0
closing_durationCzas zamykaniaNUMBER80sCzas potrzebny do pełnego zamknięcia urządzenia, w sekundach

Szczegóły:

≥ 0
return_durationCzas powrotuNUMBER0.8sCzas powrotu do pozycji lekkiego otwarcia z pozycji w pełni otwartej

Szczegóły:

≥ 0
wind_alarm_positionPozycja alarmu wiatrowegoNUMBER0%Pozycja urządzenia podczas aktywacji alarmu wiatrowego, jako procent (0-100)

Szczegóły:

≥ 0
≤ 100
motor_lock_durationCzas blokady silnikaNUMBER0.5sCzas wymagany do zwolnienia blokady silnika między zmianami kierunku, w sekundach

Szczegóły:

≥ 0

# Stan

IDNazwaTypDomyślnieJednostkaOpis
positionPozycjaNUMBER0.0%Surowa wartość pozycji wyjściowej jako procent (0-100)
desired_positionŻądana pozycjaNUMBER0.0%Żądana pozycja do przekazywania między wywołaniami
slatsPozycja lameliNUMBER0.0%Surowa wartość pozycji lameli jako procent (0-100)
desired_slatsŻądana pozycja lameliNUMBER0.0%Żądana pozycja lameli do przekazywania między wywołaniami
steps_leftPozostałe krokiNUMBER0stepsLiczba kroków pozostałych do zakończenia bieżącego ruchu
steps_left_slatsPozostałe kroki dla lameliNUMBER0stepsLiczba kroków pozostałych do zakończenia bieżącego ruchu dla lameli
was_openingBył otwieranyBOOLEANfalseWskazuje czy poprzedni ruch był w kierunku otwierania
last_movement_tsZnacznik czasu ostatniego ruchuNUMBER-60000millisecondsZnacznik czasu ostatniego ruchu używany do pauzy blokady silnika

# Kod źródłowy

Pokaż kod Volang
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
}
Steruje urządzeniami zacieniającymi takimi jak żaluzje, zasłony i markizy