# Light

Process

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

# Implementing blocks

NameIDDescription
TP-Link Tapotapo_lightDrives a TP-Link Tapo bulb or light strip over the local network, using the encrypted KLAP protocol of current Tapo firmware (L510, L530, L535, L610, L630, L900, L920 and similar). Color and white range are read from the device.
Light
P
BR
CM
CT
H
S
P
BR
CM
CT
H
S
F

# Inputs

IDAbbrevNameTypeDefaultDescription
powerPPowerBOOLEANfalseSwitches the light on when high and off when low
brightnessBRBrightnessNUMBER100Requested brightness, 1 to 100 %
color_modeCMColor modeBOOLEANfalseLow - white light at the requested color temperature, high - color given by hue and saturation. A lamp without color stays white.
color_tempCTColor temperatureNUMBER4000Requested color temperature of the white light in kelvins, narrowed down to what the lamp supports. Ignored by lamps without adjustable white.
hueHHueNUMBER0Requested hue in color mode, 0 to 360 °
saturationSSaturationNUMBER100Requested saturation in color mode, 0 to 100 %

# Outputs

IDAbbrevNameTypeDefaultDescription
powerPPowerBOOLEANfalseHigh when the light is on. Reported by the lamp when the implementation provides feedback, otherwise the commanded state.
brightnessBRBrightnessNUMBER0Brightness of the lamp in %
color_modeCMColor modeBOOLEANfalseHigh when the lamp shows a color, low when it shows white light
color_tempCTColor temperatureNUMBER0Color temperature of the white light in kelvins. The commanded value while the lamp shows a color or does not report it.
hueHHueNUMBER0Hue of the lamp in °. The commanded value when the lamp does not report it.
saturationSSaturationNUMBER0Saturation of the lamp in %. The commanded value when the lamp does not report it.
faultFFaultNUMBER00 - no fault, -1 - lamp unreachable, above 0 - implementation specific error code

# Configuration

IDNameTypeDefaultUnitDescription
featuresFeaturesENUM3What the lamp can do, each level including the previous ones: on/off, brightness, white color temperature, color. Inputs beyond it are ignored and the matching outputs read -1 (color mode low).

Details:

Values: On/Off, Dimmable, Tunable white, Color

# State

IDNameTypeDefaultUnitDescription
unit_powerReported powerNUMBER-1Power state reported by the implementation block: -1 unknown, 0 off, 1 on
unit_brightnessReported brightnessNUMBER-1Brightness reported by the implementation block, -1 when unknown
unit_color_modeReported color modeNUMBER-1Color mode reported by the implementation block: -1 unknown, 0 white, 1 color
unit_color_tempReported color temperatureNUMBER-1Color temperature reported by the implementation block, -1 when unknown
unit_hueReported hueNUMBER-1Hue reported by the implementation block, -1 when unknown
unit_saturationReported saturationNUMBER-1Saturation reported by the implementation block, -1 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 lamp reports none
sentState sentBOOLEANfalseSet once the state has been accepted by the lamp at least once
sent_powerSent powerBOOLEANfalseLast power state accepted by the lamp
sent_brightnessSent brightnessNUMBER0Last brightness accepted by the lamp
sent_color_modeSent color modeBOOLEANfalseLast color mode accepted by the lamp
sent_color_tempSent color temperatureNUMBER0Last color temperature accepted by the lamp
sent_hueSent hueNUMBER0Last hue accepted by the lamp
sent_saturationSent saturationNUMBER0Last saturation accepted by the lamp

# Source Code

View Volang source
// Light - abstract block.
//
// Turns the input channels into one desired light state and hands it to the
// attached implementation (driver), the only part that knows how to reach a
// particular lamp (Wi-Fi bulb, DALI, Zigbee, ...).
//
// Value encodings, shared by the inputs, the outputs and the driver call:
//   brightness  1..100 %
//   color_mode  false white light at color_temp, true color at hue/saturation
//   color_temp  K, the driver narrows it to what the lamp can do
//   hue         0..360 °
//   saturation  0..100 %
//   fault       0 none, -1 lamp unreachable, > 0 driver specific error code
// The features config tells what the lamp can do, each level including the
// previous ones:
//   0 on/off, 1 dimmable (brightness), 2 tunable white (color_temp),
//   3 color (color_mode, hue, saturation)
// The values beyond it are passed to the driver and published as -1, with
// color_mode low. A lamp that still cannot do what was asked for falls back
// to its closest equivalent - one without color stays white, one without a
// color temperature keeps its own - and that mapping belongs to the driver.
//
// The driver must declare one function:
//
//   light_apply(power, brightness, color_mode, color_temp, hue, saturation)
//       Pushes the complete desired state, called whenever a value changes.
//       A value of -1 means the lamp keeps its own setting.
//       It only has to accept the work, not finish it: a driver on the network
//       returns as soon as the request is out, and one still busy with an
//       earlier exchange keeps the state and sends it once that is over.
//       Returns 0 when accepted, otherwise an error code that is published on
//       the fault output.
//
// Whether and how often the lamp is polled for its status is up to the
// driver, which can be scheduled on its own. Either way it reports back through
// this block's state, then returns non-zero from the run that stored the
// readings, so the runtime calls std::on_driver_update here:
//   unit_power        -1 unknown, 0 off, 1 on
//   unit_brightness   -1 unknown, otherwise %
//   unit_color_mode   -1 unknown, 0 white, 1 color
//   unit_color_temp   -1 unknown, otherwise K
//   unit_hue          -1 unknown, otherwise °
//   unit_saturation   -1 unknown, otherwise %
//   unit_fault        the fault value to publish
// Whatever is left unknown is published as the value this block commanded, so
// a lamp without feedback simply never touches these.

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

fn publish(power, brightness, color_mode, color_temp, hue, saturation) {
    features = config::get("features")
    unit_power = state::get("unit_power")
    if (unit_power >= 0) {
        power = unit_power != 0
    }
    unit_brightness = state::get("unit_brightness")
    if (unit_brightness >= 0 and features >= 1) {
        brightness = unit_brightness
    }
    unit_color_mode = state::get("unit_color_mode")
    if (unit_color_mode >= 0 and features >= 3) {
        color_mode = unit_color_mode != 0
    }
    unit_color_temp = state::get("unit_color_temp")
    if (unit_color_temp >= 0 and features >= 2) {
        color_temp = unit_color_temp
    }
    unit_hue = state::get("unit_hue")
    if (unit_hue >= 0 and features >= 3) {
        hue = unit_hue
    }
    unit_saturation = state::get("unit_saturation")
    if (unit_saturation >= 0 and features >= 3) {
        saturation = unit_saturation
    }
    fault = state::get("unit_fault")
    if (fault == 0) {
        fault = state::get("invoke_rc")
    }
    output::set("power", power)
    output::set("brightness", brightness)
    output::set("color_mode", color_mode)
    output::set("color_temp", color_temp)
    output::set("hue", hue)
    output::set("saturation", saturation)
    output::set("fault", fault)
}

fn changed(power, brightness, color_mode, color_temp, hue, saturation) {
    if (!state::get("sent")) {
        return true
    }
    if (state::get("sent_power") != power) {
        return true
    }
    if (state::get("sent_brightness") != brightness) {
        return true
    }
    if (state::get("sent_color_mode") != color_mode) {
        return true
    }
    if (state::get("sent_color_temp") != color_temp) {
        return true
    }
    if (state::get("sent_hue") != hue) {
        return true
    }
    if (state::get("sent_saturation") != saturation) {
        return true
    }
    return false
}

// The last readings describe the lamp before a command, so they must not mask
// what was just asked for; the read that follows fills them in again.
fn forget_readings() {
    state::set("unit_power", -1)
    state::set("unit_brightness", -1)
    state::set("unit_color_mode", -1)
    state::set("unit_color_temp", -1)
    state::set("unit_hue", -1)
    state::set("unit_saturation", -1)
}

fn remember(power, brightness, color_mode, color_temp, hue, saturation) {
    state::set("sent_power", power)
    state::set("sent_brightness", brightness)
    state::set("sent_color_mode", color_mode)
    state::set("sent_color_temp", color_temp)
    state::set("sent_hue", hue)
    state::set("sent_saturation", saturation)
    state::set("sent", true)
}

// Called by the runtime after the driver stored fresh readings. The commanded
// values are only a fallback for whatever the lamp did not report.
extern fn std::on_driver_update() {
    power = state::get("sent_power")
    brightness = state::get("sent_brightness")
    color_mode = state::get("sent_color_mode")
    color_temp = state::get("sent_color_temp")
    hue = state::get("sent_hue")
    saturation = state::get("sent_saturation")
    publish(power, brightness, color_mode, color_temp, hue, saturation)
}

power = input::get("power")
brightness = clamp_int(input::get("brightness"), 1, 100)
color_mode = input::get("color_mode")
color_temp = clamp_int(input::get("color_temp"), 1000, 10000)
hue = clamp_int(input::get("hue"), 0, 360)
saturation = clamp_int(input::get("saturation"), 0, 100)
features = config::get("features")
if (features < 1) {
    brightness = -1
}
if (features < 2) {
    color_temp = -1
}
if (features < 3) {
    color_mode = false
    hue = -1
    saturation = -1
}

if (changed(power, brightness, color_mode, color_temp, hue, saturation)) {
    // 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, brightness, color_mode, color_temp, hue, saturation)
    rc = std::invoke("light_apply", power, brightness, color_mode, color_temp, hue, saturation)
    state::set("invoke_rc", rc)
    if (rc == 0) {
        remember(power, brightness, color_mode, color_temp, hue, saturation)
        forget_readings()
    }
}
publish(power, brightness, color_mode, color_temp, hue, saturation)
Learn how the Light logic block works, when to use it, and how to configure it in your Voldeno smart home automation.