# Light
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
| Name | ID | Description |
|---|---|---|
| TP-Link Tapo | tapo_light | Drives 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
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
power | P | Power | BOOLEAN | false | Switches the light on when high and off when low |
brightness | BR | Brightness | NUMBER | 100 | Requested brightness, 1 to 100 % |
color_mode | CM | Color mode | BOOLEAN | false | Low - white light at the requested color temperature, high - color given by hue and saturation. A lamp without color stays white. |
color_temp | CT | Color temperature | NUMBER | 4000 | Requested color temperature of the white light in kelvins, narrowed down to what the lamp supports. Ignored by lamps without adjustable white. |
hue | H | Hue | NUMBER | 0 | Requested hue in color mode, 0 to 360 ° |
saturation | S | Saturation | NUMBER | 100 | Requested saturation in color mode, 0 to 100 % |
# Outputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
power | P | Power | BOOLEAN | false | High when the light is on. Reported by the lamp when the implementation provides feedback, otherwise the commanded state. |
brightness | BR | Brightness | NUMBER | 0 | Brightness of the lamp in % |
color_mode | CM | Color mode | BOOLEAN | false | High when the lamp shows a color, low when it shows white light |
color_temp | CT | Color temperature | NUMBER | 0 | Color temperature of the white light in kelvins. The commanded value while the lamp shows a color or does not report it. |
hue | H | Hue | NUMBER | 0 | Hue of the lamp in °. The commanded value when the lamp does not report it. |
saturation | S | Saturation | NUMBER | 0 | Saturation of the lamp in %. The commanded value when the lamp does not report it. |
fault | F | Fault | NUMBER | 0 | 0 - no fault, -1 - lamp unreachable, above 0 - implementation specific error code |
# Configuration
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
features | Features | ENUM | 3 | What 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
| 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_brightness | Reported brightness | NUMBER | -1 | Brightness reported by the implementation block, -1 when unknown | |
unit_color_mode | Reported color mode | NUMBER | -1 | Color mode reported by the implementation block: -1 unknown, 0 white, 1 color | |
unit_color_temp | Reported color temperature | NUMBER | -1 | Color temperature reported by the implementation block, -1 when unknown | |
unit_hue | Reported hue | NUMBER | -1 | Hue reported by the implementation block, -1 when unknown | |
unit_saturation | Reported saturation | NUMBER | -1 | Saturation reported by the implementation block, -1 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 lamp reports none | |
sent | State sent | BOOLEAN | false | Set once the state has been accepted by the lamp at least once | |
sent_power | Sent power | BOOLEAN | false | Last power state accepted by the lamp | |
sent_brightness | Sent brightness | NUMBER | 0 | Last brightness accepted by the lamp | |
sent_color_mode | Sent color mode | BOOLEAN | false | Last color mode accepted by the lamp | |
sent_color_temp | Sent color temperature | NUMBER | 0 | Last color temperature accepted by the lamp | |
sent_hue | Sent hue | NUMBER | 0 | Last hue accepted by the lamp | |
sent_saturation | Sent saturation | NUMBER | 0 | Last 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)