# Air conditioner
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
| Name | ID | Description |
|---|---|---|
| Daikin BRP069 | daikin_brp069 | Drives 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
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
power | P | Power | BOOLEAN | false | Switches the unit on when high and off when low |
mode | M | Mode | NUMBER | 0 | Operating mode: 0 - auto, 1 - heating, 2 - cooling, 3 - drying, 4 - fan only. A unit without the requested mode uses its closest equivalent. |
target_temp | TT | Target temperature | NUMBER | 21 | Requested target temperature, limited to the configured range |
fan_speed | FS | Fan speed | NUMBER | 0 | Fan speed: 0 - auto, 1 - quiet, 2 - low, 3 - medium, 4 - high, 5 - powerful (boost). A unit with fewer steps uses its closest equivalent. |
swing_v | SV | Vertical airflow | NUMBER | 0 | Vertical vane: 0 - auto, 1 - swing, 2 to 6 - fixed position from the highest to the lowest |
swing_h | SH | Horizontal airflow | NUMBER | 0 | Horizontal vane: 0 - auto, 1 - swing, 2 to 6 - fixed position from the leftmost to the rightmost |
air_clean | CLN | Air cleaning | BOOLEAN | false | Enables the air cleaning feature of the unit (ionizer, streamer, plasma filter). Ignored by units without one. |
# Outputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
power | P | Power | BOOLEAN | false | High when the unit is on. Reported by the unit when the implementation provides feedback, otherwise the commanded state. |
mode | M | Mode | NUMBER | 0 | Effective 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_temp | TT | Target temperature | NUMBER | 0 | Target temperature the unit works with, after the configured range has been applied |
fan_speed | FS | Fan speed | NUMBER | 0 | Fan 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_v | SV | Vertical airflow | NUMBER | 0 | Vertical 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_h | SH | Horizontal airflow | NUMBER | 0 | Horizontal 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_clean | CLN | Air cleaning | BOOLEAN | false | Air cleaning setting the unit was asked for. No unit reports this back, so it is always the commanded value. |
room_temp | RT | Room temperature | NUMBER | -1000 | Indoor temperature measured by the unit. -1000 when the unit does not report it. |
fault | F | Fault | NUMBER | 0 | 0 - no fault, -1 - unit unreachable, above 0 - unit specific error code |
# Configuration
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
min_target_temp | Minimum target temperature | NUMBER | 10 | °C | Lower limit for the target temperature input. The unit may narrow it down further. Details: ≤ max_target_temp |
max_target_temp | Maximum target temperature | NUMBER | 30 | °C | Upper limit for the target temperature input. The unit may narrow it down further. Details: ≥ min_target_temp |
refresh_interval | Refresh interval | NUMBER | 60 | s | How 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_refresh | Resend state on refresh | BOOLEAN | false | Sends 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
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
unit_power | Reported power | NUMBER | -1 | Power state reported by the implementation block: -1 unknown, 0 off, 1 on | |
unit_mode | Reported mode | NUMBER | -1 | Mode reported by the implementation block, -1 when unknown | |
unit_target_temp | Reported target temperature | NUMBER | -1000.0 | °C | Target temperature reported by the implementation block, -1000 when unknown |
unit_room_temp | Reported room temperature | NUMBER | -1000.0 | °C | Indoor temperature reported by the implementation block, -1000 when unknown |
unit_fault | Reported fault | NUMBER | 0 | Fault reported by the implementation block | |
invoke_rc | Last implementation result | NUMBER | 0 | Value returned by the last implementation call, published as a fault when the unit reports none | |
sent | State sent | BOOLEAN | false | Set once the state has been accepted by the unit at least once | |
sent_power | Sent power | BOOLEAN | false | Last power state accepted by the unit | |
sent_mode | Sent mode | NUMBER | 0 | Last mode accepted by the unit | |
sent_target_temp | Sent target temperature | NUMBER | 0.0 | °C | Last target temperature accepted by the unit |
sent_fan_speed | Sent fan speed | NUMBER | 0 | Last fan speed accepted by the unit | |
sent_swing_v | Sent vertical airflow | NUMBER | 0 | Last vertical vane setting accepted by the unit | |
sent_swing_h | Sent horizontal airflow | NUMBER | 0 | Last horizontal vane setting accepted by the unit | |
sent_air_clean | Sent air cleaning | BOOLEAN | false | Last air cleaning setting accepted by the unit | |
last_activation_ts | Last execution timestamp | NUMBER | 0 | s | Last execution timestamp in seconds from epoch |
next_activation_ts | Next execution timestamp | NUMBER | 0 | s | Next 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() == "")