# Air conditioner

Process scheduler

Generic air conditioner control. Holds the logic common to every unit and delegates the communication to an attached implementation block, which is the only part that knows a given brand or model.

# Implementing blocks

NameIDDescription
Daikin BRP069daikin_brp069Drives a Daikin air conditioner over the local network, using the plain HTTP API of the BRP069 family of Wi-Fi adapters (BRP069A4x, BRP069B4x, BRP072A4x). The newer BRP072C4x and BRP084 adapters speak different protocols and need their own driver.
Air conditioner
P
M
TT
FS
SV
SH
CLN
P
M
TT
FS
SV
SH
CLN
RT
F

# Inputs

IDAbbrevNameTypeDefaultDescription
powerPPowerBOOLEANfalseSwitches the unit on when high and off when low
modeMModeNUMBER0Operating mode: 0 - auto, 1 - heating, 2 - cooling, 3 - drying, 4 - fan only. A unit without the requested mode uses its closest equivalent.
target_tempTTTarget temperatureNUMBER21Requested target temperature, limited to the configured range
fan_speedFSFan speedNUMBER0Fan speed: 0 - auto, 1 - quiet, 2 - low, 3 - medium, 4 - high, 5 - powerful (boost). A unit with fewer steps uses its closest equivalent.
swing_vSVVertical airflowNUMBER0Vertical vane: 0 - auto, 1 - swing, 2 to 6 - fixed position from the highest to the lowest
swing_hSHHorizontal airflowNUMBER0Horizontal vane: 0 - auto, 1 - swing, 2 to 6 - fixed position from the leftmost to the rightmost
air_cleanCLNAir cleaningBOOLEANfalseEnables the air cleaning feature of the unit (ionizer, streamer, plasma filter). Ignored by units without one.

# Outputs

IDAbbrevNameTypeDefaultDescription
powerPPowerBOOLEANfalseHigh when the unit is on. Reported by the unit when the implementation provides feedback, otherwise the commanded state.
modeMModeNUMBER0Effective operating mode, using the same values as the mode input. Units that report their status resolve the auto mode into heating or cooling here.
target_tempTTTarget temperatureNUMBER0Target temperature the unit works with, after the configured range has been applied
fan_speedFSFan speedNUMBER0Fan speed the unit was asked for, using the same values as the fan speed input. No unit reports this back, so it is always the commanded value.
swing_vSVVertical airflowNUMBER0Vertical vane setting the unit was asked for, using the same values as the vertical airflow input. No unit reports this back, so it is always the commanded value.
swing_hSHHorizontal airflowNUMBER0Horizontal vane setting the unit was asked for, using the same values as the horizontal airflow input. No unit reports this back, so it is always the commanded value.
air_cleanCLNAir cleaningBOOLEANfalseAir cleaning setting the unit was asked for. No unit reports this back, so it is always the commanded value.
room_tempRTRoom temperatureNUMBER-1000Indoor temperature measured by the unit. -1000 when the unit does not report it.
faultFFaultNUMBER00 - no fault, -1 - unit unreachable, above 0 - unit specific error code

# Configuration

IDNameTypeDefaultUnitDescription
min_target_tempMinimum target temperatureNUMBER10°CLower limit for the target temperature input. The unit may narrow it down further.

Details:

≤ max_target_temp
max_target_tempMaximum target temperatureNUMBER30°CUpper limit for the target temperature input. The unit may narrow it down further.

Details:

≥ min_target_temp
refresh_intervalRefresh intervalNUMBER60sHow often the unit status is read, in seconds. Set to 0 to never read it, for example for units controlled over infrared.

Details:

≥ 0
resend_on_refreshResend state on refreshBOOLEANfalseSends the full state to the unit on every refresh instead of only after a change. Keeps units without feedback in sync after they were operated with their own remote control.

# State

IDNameTypeDefaultUnitDescription
unit_powerReported powerNUMBER-1Power state reported by the implementation block: -1 unknown, 0 off, 1 on
unit_modeReported modeNUMBER-1Mode reported by the implementation block, -1 when unknown
unit_target_tempReported target temperatureNUMBER-1000.0°CTarget temperature reported by the implementation block, -1000 when unknown
unit_room_tempReported room temperatureNUMBER-1000.0°CIndoor temperature reported by the implementation block, -1000 when unknown
unit_faultReported faultNUMBER0Fault reported by the implementation block
invoke_rcLast implementation resultNUMBER0Value returned by the last implementation call, published as a fault when the unit reports none
sentState sentBOOLEANfalseSet once the state has been accepted by the unit at least once
sent_powerSent powerBOOLEANfalseLast power state accepted by the unit
sent_modeSent modeNUMBER0Last mode accepted by the unit
sent_target_tempSent target temperatureNUMBER0.0°CLast target temperature accepted by the unit
sent_fan_speedSent fan speedNUMBER0Last fan speed accepted by the unit
sent_swing_vSent vertical airflowNUMBER0Last vertical vane setting accepted by the unit
sent_swing_hSent horizontal airflowNUMBER0Last horizontal vane setting accepted by the unit
sent_air_cleanSent air cleaningBOOLEANfalseLast air cleaning setting accepted by the unit
last_activation_tsLast execution timestampNUMBER0sLast execution timestamp in seconds from epoch
next_activation_tsNext execution timestampNUMBER0sNext execution timestamp in seconds from epoch

# Source Code

View Volang source
// 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() == "")
Learn how the Air conditioner logic block works, when to use it, and how to configure it in your Voldeno smart home automation.