# Air conditioner

Przetwarzanie scheduler

Ogólne sterowanie klimatyzatorem. Zawiera logikę wspólną dla wszystkich urządzeń, a komunikację deleguje do dołączonego bloku implementacji, który jako jedyny zna daną markę lub model.

# Bloki implementujące

NazwaIDOpis
Daikin BRP069daikin_brp069Steruje klimatyzatorem Daikin w sieci lokalnej, korzystając z nieszyfrowanego API HTTP adapterów Wi-Fi z rodziny BRP069 (BRP069A4x, BRP069B4x, BRP072A4x). Nowsze adaptery BRP072C4x i BRP084 używają innych protokołów i wymagają własnych bloków.
Air conditioner
P
M
TT
FS
SV
SH
CLN
P
M
TT
FS
SV
SH
CLN
RT
F

# Wejścia

IDSkrótNazwaTypDomyślnieOpis
powerPZasilanieBOOLEANfalseWłącza urządzenie przy stanie wysokim, wyłącza przy niskim
modeMTrybNUMBER0Tryb pracy: 0 - auto, 1 - grzanie, 2 - chłodzenie, 3 - osuszanie, 4 - tylko wentylator. Urządzenie bez danego trybu użyje najbliższego odpowiednika.
target_tempTTTemperatura docelowaNUMBER21Żądana temperatura docelowa, ograniczona do skonfigurowanego zakresu
fan_speedFSPrędkość wentylatoraNUMBER0Prędkość wentylatora: 0 - auto, 1 - cicho, 2 - niska, 3 - średnia, 4 - wysoka, 5 - maksymalna (boost). Urządzenie z mniejszą liczbą stopni użyje najbliższego odpowiednika.
swing_vSVNawiew pionowyNUMBER0Żaluzja pionowa: 0 - auto, 1 - oscylacja, 2 do 6 - stała pozycja od najwyższej do najniższej
swing_hSHNawiew poziomyNUMBER0Żaluzja pozioma: 0 - auto, 1 - oscylacja, 2 do 6 - stała pozycja od skrajnie lewej do skrajnie prawej
air_cleanCLNOczyszczanie powietrzaBOOLEANfalseWłącza funkcję oczyszczania powietrza w urządzeniu (jonizator, streamer, filtr plazmowy). Ignorowane przez urządzenia bez tej funkcji.

# Wyjścia

IDSkrótNazwaTypDomyślnieOpis
powerPZasilanieBOOLEANfalseStan wysoki, gdy urządzenie jest włączone. Odczytany z urządzenia, gdy implementacja udostępnia informację zwrotną, w przeciwnym razie stan zadany.
modeMTrybNUMBER0Aktualny tryb pracy, w tym samym kodowaniu co wejście trybu. Urządzenia raportujące swój stan rozwijają tutaj tryb auto na grzanie lub chłodzenie.
target_tempTTTemperatura docelowaNUMBER0Temperatura docelowa, na której pracuje urządzenie, po zastosowaniu skonfigurowanego zakresu
fan_speedFSPrędkość wentylatoraNUMBER0Zadana prędkość wentylatora, w tym samym kodowaniu co wejście prędkości wentylatora. Żadne urządzenie jej nie raportuje, więc jest to zawsze wartość zadana.
swing_vSVNawiew pionowyNUMBER0Zadane ustawienie kierownicy pionowej, w tym samym kodowaniu co wejście nawiewu pionowego. Żadne urządzenie go nie raportuje, więc jest to zawsze wartość zadana.
swing_hSHNawiew poziomyNUMBER0Zadane ustawienie kierownicy poziomej, w tym samym kodowaniu co wejście nawiewu poziomego. Żadne urządzenie go nie raportuje, więc jest to zawsze wartość zadana.
air_cleanCLNOczyszczanie powietrzaBOOLEANfalseZadane ustawienie oczyszczania powietrza. Żadne urządzenie go nie raportuje, więc jest to zawsze wartość zadana.
room_tempRTTemperatura pomieszczeniaNUMBER-1000Temperatura wewnętrzna zmierzona przez urządzenie. -1000, gdy urządzenie jej nie raportuje.
faultFAwariaNUMBER00 - brak awarii, -1 - brak komunikacji z urządzeniem, powyżej 0 - kod błędu urządzenia

# Konfiguracja

IDNazwaTypDomyślnieJednostkaOpis
min_target_tempMinimalna temperatura docelowaNUMBER10°CDolny limit wejścia temperatury docelowej. Urządzenie może go dodatkowo zawęzić.

Szczegóły:

≤ max_target_temp
max_target_tempMaksymalna temperatura docelowaNUMBER30°CGórny limit wejścia temperatury docelowej. Urządzenie może go dodatkowo zawęzić.

Szczegóły:

≥ min_target_temp
refresh_intervalInterwał odświeżaniaNUMBER60sJak często odczytywany jest stan urządzenia, w sekundach. Ustaw 0, aby go nie odczytywać, na przykład dla urządzeń sterowanych przez podczerwień.

Szczegóły:

≥ 0
resend_on_refreshWysyłaj stan przy odświeżaniuBOOLEANfalseWysyła pełny stan do urządzenia przy każdym odświeżeniu, a nie tylko po zmianie. Utrzymuje synchronizację urządzeń bez informacji zwrotnej po użyciu ich własnego pilota.

# Stan

IDNazwaTypDomyślnieJednostkaOpis
unit_powerRaportowane zasilanieNUMBER-1Stan zasilania raportowany przez blok implementacji: -1 nieznany, 0 wyłączone, 1 włączone
unit_modeRaportowany trybNUMBER-1Tryb raportowany przez blok implementacji, -1 gdy nieznany
unit_target_tempRaportowana temperatura docelowaNUMBER-1000.0°CTemperatura docelowa raportowana przez blok implementacji, -1000 gdy nieznana
unit_room_tempRaportowana temperatura pomieszczeniaNUMBER-1000.0°CTemperatura wewnętrzna raportowana przez blok implementacji, -1000 gdy nieznana
unit_faultRaportowana awariaNUMBER0Awaria raportowana przez blok implementacji
invoke_rcWynik ostatniego wywołania implementacjiNUMBER0Wartość zwrócona przez ostatnie wywołanie implementacji, publikowana jako awaria, gdy urządzenie nie raportuje własnej
sentStan wysłanyBOOLEANfalseUstawione, gdy stan został przyjęty przez urządzenie co najmniej raz
sent_powerWysłane zasilanieBOOLEANfalseOstatni stan zasilania przyjęty przez urządzenie
sent_modeWysłany trybNUMBER0Ostatni tryb przyjęty przez urządzenie
sent_target_tempWysłana temperatura docelowaNUMBER0.0°COstatnia temperatura docelowa przyjęta przez urządzenie
sent_fan_speedWysłana prędkość wentylatoraNUMBER0Ostatnia prędkość wentylatora przyjęta przez urządzenie
sent_swing_vWysłany nawiew pionowyNUMBER0Ostatnie ustawienie żaluzji pionowej przyjęte przez urządzenie
sent_swing_hWysłany nawiew poziomyNUMBER0Ostatnie ustawienie żaluzji poziomej przyjęte przez urządzenie
sent_air_cleanWysłane oczyszczanie powietrzaBOOLEANfalseOstatnie ustawienie oczyszczania powietrza przyjęte przez urządzenie
last_activation_tsOstatni znacznik czasu wykonaniaNUMBER0sOstatni znacznik czasu wykonania w sekundach od epoki
next_activation_tsNastępny znacznik czasu wykonaniaNUMBER0sNastępny znacznik czasu wykonania w sekundach od epoki

# Kod źródłowy

Pokaż kod Volang
// Air conditioner - abstract block.
//
// Turns the input channels into one desired unit state and hands it to the
// attached implementation (driver), the only part that knows how to reach a
// particular unit (IR, LAN, Modbus, ...).
//
// Value encodings, shared by the inputs, the outputs and the driver call:
//   mode       0 auto, 1 heating, 2 cooling, 3 drying, 4 fan only
//   fan_speed  0 auto, 1 quiet, 2 low, 3 medium, 4 high, 5 powerful
//   swing_v    0 auto, 1 swing, 2..6 fixed vane (2 highest .. 6 lowest)
//   swing_h    0 auto, 1 swing, 2..6 fixed vane (2 leftmost .. 6 rightmost)
//   fault      0 none, -1 unit unreachable, > 0 unit specific error code
// A unit that cannot do what was asked for falls back to its closest
// equivalent - that mapping belongs to the driver.
//
// The driver must declare both functions. They only have to accept the work,
// not finish it: one on the network returns as soon as the request is out.
//
//   ac_apply(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
//       Pushes the complete desired state and leaves the readings refreshed, so
//       a tick that sends a command does not poll on top of it. Called whenever
//       a value changes, and on every refresh tick when resend_on_refresh is set.
//   ac_refresh()
//       Asks the unit for its status, on the scheduled run the runtime performs
//       every refresh_interval seconds. Units without feedback (IR) return 0.
//
// Both return 0 accepted, < 0 busy - retried a second later and not a fault,
// > 0 failed and published on the fault output.
//
// The driver shares this block's state, which is how it reports back:
//   unit_power        -1 unknown, 0 off, 1 on
//   unit_mode         -1 unknown, otherwise a mode value
//   unit_target_temp  -1000 unknown, otherwise °C
//   unit_room_temp    -1000 unknown, otherwise °C
//   unit_fault        the fault value to publish
// Whatever is left unknown is published as the value this block commanded.

fn choice(value, hi) {
    v = math::int(math::round(value))
    if (v < 0) {
        return 0
    }
    if (v > hi) {
        return hi
    }
    return v
}

fn clamp(value, lo, hi) {
    if (value < lo) {
        return lo
    }
    if (value > hi) {
        return hi
    }
    return value
}

// Airflow and air cleaning have no counterpart in the readings - no unit
// reports them back - so they are published exactly as commanded.
fn publish(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean) {
    unit_power = state::get("unit_power")
    if (unit_power >= 0) {
        power = unit_power != 0
    }
    unit_mode = state::get("unit_mode")
    if (unit_mode >= 0) {
        mode = unit_mode
    }
    unit_target_temp = state::get("unit_target_temp")
    if (unit_target_temp > -1000) {
        target_temp = unit_target_temp
    }
    fault = state::get("unit_fault")
    if (fault == 0) {
        rc = state::get("invoke_rc")
        // A negative code only means the driver was busy, which the retry on the
        // next run takes care of.
        if (rc > 0) {
            fault = rc
        }
    }
    output::set("power", power)
    output::set("mode", mode)
    output::set("target_temp", target_temp)
    output::set("fan_speed", fan_speed)
    output::set("swing_v", swing_v)
    output::set("swing_h", swing_h)
    output::set("air_clean", air_clean)
    output::set("room_temp", state::get("unit_room_temp"))
    output::set("fault", fault)
}

fn changed(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean) {
    if (!state::get("sent")) {
        return true
    }
    if (state::get("sent_power") != power) {
        return true
    }
    if (state::get("sent_air_clean") != air_clean) {
        return true
    }
    if (state::get("sent_mode") != mode) {
        return true
    }
    if (state::get("sent_target_temp") != target_temp) {
        return true
    }
    if (state::get("sent_fan_speed") != fan_speed) {
        return true
    }
    if (state::get("sent_swing_v") != swing_v) {
        return true
    }
    if (state::get("sent_swing_h") != swing_h) {
        return true
    }
    return false
}

// The last readings describe the unit before a command, so they must not mask
// what was just asked for; the read that follows fills them in again. The room
// temperature is a measurement, not a setting, and stays.
fn forget_readings() {
    state::set("unit_power", -1)
    state::set("unit_mode", -1)
    state::set("unit_target_temp", -1000.0)
}

fn remember(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean) {
    state::set("sent_power", power)
    state::set("sent_mode", mode)
    state::set("sent_target_temp", target_temp)
    state::set("sent_fan_speed", fan_speed)
    state::set("sent_swing_v", swing_v)
    state::set("sent_swing_h", swing_h)
    state::set("sent_air_clean", air_clean)
    state::set("sent", true)
}

// The refresh runs on the runtime's activation, so the block's one timer is
// free for the retry below.
fn arm_tick(delay_ms) {
    callback::clear()
    callback::set(delay_ms, "on_refresh")
}

fn run(refresh) {
    power = input::get("power")
    mode = choice(input::get("mode"), 4)
    fan_speed = choice(input::get("fan_speed"), 5)
    swing_v = choice(input::get("swing_v"), 6)
    swing_h = choice(input::get("swing_h"), 6)
    air_clean = input::get("air_clean")
    min_temp = config::get("min_target_temp")
    max_temp = config::get("max_target_temp")
    target_temp = clamp(input::get("target_temp"), min_temp, max_temp)

    stale = true
    if (!refresh) {
        // Report the commanded state right away, so the outputs stay meaningful
        // even when the driver call below fails or no driver is attached at all.
        publish(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
        stale = false
    }

    // A command comes before a status poll: a driver runs one exchange at a time
    // and would turn the poll down. It refreshes the readings itself.
    applied = false
    resend = refresh and config::get("resend_on_refresh")
    if (changed(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean) or resend) {
        rc = std::invoke("ac_apply", power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
        state::set("invoke_rc", rc)
        if (rc == 0) {
            remember(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
            forget_readings()
        }
        if (rc < 0) {
            // The driver is still busy with an earlier exchange. Come back
            // shortly rather than leave the command until the next refresh.
            arm_tick(1000)
        }
        applied = true
        stale = true
    }

    if (refresh and !applied) {
        state::set("invoke_rc", std::invoke("ac_refresh"))
        stale = true
    }

    if (stale) {
        publish(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
    }
}

// Asked by the runtime for the next refresh, in whole seconds - the only form
// the scheduler reads back. 0 turns polling off for units that report nothing.
extern fn std::next_activation_at() {
    interval = math::int(math::round(config::get("refresh_interval")))
    if (interval <= 0) {
        return 0
    }
    return time::now() + interval
}

extern fn on_refresh() {
    run(true)
}

// Called by the runtime after the driver stored fresh readings. The commanded
// values are only a fallback for whatever the unit did not report.
extern fn std::on_driver_update() {
    power = state::get("sent_power")
    mode = state::get("sent_mode")
    target_temp = state::get("sent_target_temp")
    fan_speed = state::get("sent_fan_speed")
    swing_v = state::get("sent_swing_v")
    swing_h = state::get("sent_swing_h")
    air_clean = state::get("sent_air_clean")
    publish(power, mode, target_temp, fan_speed, swing_v, swing_h, air_clean)
}

// A scheduled run arrives without a channel, and that is the refresh tick.
run(input::channel() == "")
Poznaj działanie bloku logicznego Air conditioner - zasada działania, opcje konfiguracji i przykłady zastosowań w Voldeno.