LIFX Driver Code

The LIFX Classes tightly follow the LIFX Lan Protocol and are similar in structure to the Z-Wave classes.

Classes:

hubitat.lifx.commands.GetColor
   
   byte[] getPayload()
   String format()
hubitat.lifx.commands.SetColor
   Integer hue
   Integer saturation
   Integer brightness
   Integer kelvin
   Long duration

   byte[] getPayload()
   String format()
hubitat.lifx.commands.LightState
   Integer hue
   Integer saturation
   Integer brightness
   Integer kelvin
   Integer power
   String label

   byte[] getPayload()
   String format()
hubitat.lifx.commands.GetLightPower
   
   byte[] getPayload()
   String format()
hubitat.lifx.commands.SetLightPower
   Integer level
   Integer duration

   byte[] getPayload()
   String format()
hubitat.lifx.commands.StateLightPower
   Integer level

   byte[] getPayload()
   String format()

Every packet type specified in the LIFX Lan Protocol documentation has a class implemented. I will be documenting all of them, but if you need one sooner than the documentation is complete, please let me know which ones you need.

Sample Driver:

import hubitat.helper.ColorUtils

metadata {
    definition (name: "LIFX Color", namespace: "hubitat", author: "Bryan Copeland") {
        capability "SwitchLevel"
        capability "ColorTemperature"
        capability "ColorControl"
        capability "Switch"
        capability "Refresh"
        capability "Actuator"
        capability "SignalStrength"
        capability "Configuration"
        capability "ColorMode"
        capability "Polling"

        command "flash"

    }
    preferences {
        input name: "pollInterval", type: "enum", title: "Poll Interval", defaultValue: 0, options: [0: "Disabled", 5: "5 Minutes", 10: "10 Minutes", 15: "15 Minutes", 30: "30 Minutes", 60: "1 Hour"], submitOnChange: true, width: 8
        input name: "colorTransition", type: "enum", description: "", title: "Transition Time", defaultValue: 0, options: [0: "ASAP", 1: "1 second", 2: "2 seconds", 3: "3 seconds", 4: "4 seconds", 5: "5 seconds", 6: "6 seconds", 7: "7 seconds", 8: "8 seconds", 9: "9 seconds", 10: "10 seconds"]
        input name: "flashRate", type: "enum", title: "Flash rate", options:[[750:"750ms"],[1000:"1s"],[2000:"2s"],[5000:"5s"]], defaultValue: 750
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
    }
}


void logsOff(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

void configure() {
    if (logEnable) log.debug "configure()"
    List<String> cmds = []
    cmds.add(new hubitat.lifx.commands.GetColor().format())
    cmds.add(new hubitat.lifx.commands.GetHostFirmware().format())
    cmds.add(new hubitat.lifx.commands.GetWifiFirmware().format())
    cmds.add(new hubitat.lifx.commands.GetVersion().format())
    sendToDevice(cmds)
    refresh()
}

void updated() {
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${txtEnable == true}"
    unschedule()
    if (pollInterval) {
        switch (pollInterval.toInteger()) {
            case 5:
                runEvery5Minutes("poll")
                break;
            case 10:
                runEvery10Minutes("poll")
                break;
            case 15:
                runEvery15Minutes("poll")
                break;
            case 30:
                runEvery30Minutes("poll")
                break;
            case 60:
                runEvery60Minutes("poll")
                break;
        }
    }
    if (logEnable) runIn(1800,logsOff)
}

void parse(String description) {
    if (logEnable) log.debug "parse:${description}"
    hubitat.lifx.Command cmd = hubitat.lifx.Lifx.parse(description)
    if (cmd) {
        lifxEvent(cmd)
    }
}

void eventProcess(Map evt) {
    if (device.currentValue(evt.name).toString() != evt.value.toString()) {
        sendEvent(evt)
        if (txtEnable && evt.descriptionText) log.info evt.descriptionText
        if (txtEnable && !evt.descriptionText) {
            if (evt.unit == null) evt.unit = ""
            log.info "${device.displayName} ${evt.name} is ${evt.value}${evt.unit}"
        }
    }
}

void lifxEvent(hubitat.lifx.Command cmd) {
    if (logEnable) log.debug "Unhandled Command: ${cmd}"
}

void refresh() {
    if (logEnable) log.debug "refresh()"
    List<String> cmds = []
    cmds.add(new hubitat.lifx.commands.GetColor().format())
    cmds.add(new hubitat.lifx.commands.GetWifiInfo().format())
    sendToDevice(cmds)
}

void privateRefresh() {
    sendToDevice(new hubitat.lifx.commands.GetColor().format())
}

void poll() {
    privateRefresh()
}

void flash() {
    if (logEnable) log.debug "flash()"
    String descriptionText = "${device.getDisplayName()} was set to flash with a rate of ${flashRate ?: 750} milliseconds"
    if (txtEnable) log.info "${descriptionText}"
    state.flashing = true
    flashOn()
}

void flashOn() {
    if (!state.flashing) return
    runInMillis((flashRate ?: 750).toInteger(), flashOff)
    sendToDevice(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: 0).format())
}

void flashOff() {
    if (!state.flashing) return
    runInMillis((flashRate ?: 750).toInteger(), flashOn)
    sendToDevice(new hubitat.lifx.commands.SetLightPower(level: 0, duration: 0).format())
}

void on() {
    state.flashing = false
    if (logEnable) log.debug "on()"
    Integer tt = 1000
    if (colorTransition) tt = colorTransition.toInteger() * 1000
    sendToDevice(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void off() {
    state.flashing = false
    if (logEnable) log.debug "off()"
    Integer tt = 1000
    if (colorTransition) tt = colorTransition.toInteger() * 1000
    sendToDevice(new hubitat.lifx.commands.SetLightPower(level: 0, duration: tt).format())
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void setLevel(value, duration=null) {
    state.flashing = false
    List<String> cmds = []
    if (logEnable) log.debug "setLevel(${value})"
    Integer tt = 1000
    if (duration != null) {
        tt = duration * 1000
    } else {
        if (colorTransition) tt = colorTransition.toInteger() * 1000
    }
    cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(device.currentValue("hue") * 655.35), saturation: Math.round(device.currentValue("saturation") * 655.35), brightness: Math.round(value * 655.35), kelvin: device.currentValue("colorTemperature"), duration: tt).format())
    cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    sendToDevice(cmds)
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void setHue(value) {
    state.flashing = false
    List<String> cmds = []
    if (logEnable) log.debug "setHue(${value})"
    Integer tt = 1000
    if (colorTransition) tt = colorTransition.toInteger() * 1000
    cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(value * 655.35), saturation: Math.round(device.currentValue("saturation") * 655.35), brightness: Math.round(device.currentValue("level") * 655.35), kelvin: device.currentValue("colorTemperature"), duration: tt).format())
    cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    sendToDevice(cmds)
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void setSaturation(value) {
    state.flashing = false
    List<String> cmds = []
    if (logEnable) log.debug "setSaturation(${value})"
    Integer tt = 1000
    if (colorTransition) tt = colorTransition.toInteger() * 1000
    cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(device.currentValue("hue") * 655.35), saturation: Math.round(value * 655.35), brightness: Math.round(device.currentValue("level") * 655.35), kelvin: device.currentValue("colorTemperature"), duration: tt).format())
    cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    sendToDevice(cmds)
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void setColor(value) {
    state.flashing = false
    List<String> cmds = []
    if (logEnable) log.debug "setColor(${value})"
    Integer tt = 1000
    if (colorTransition) tt = colorTransition.toInteger() * 1000
    if (value.hue == null || value.saturation == null) return
    if (value.level == null) value.level=100
    cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(value.hue * 655.35), saturation: Math.round(value.saturation * 655.35), brightness: Math.round(value.level * 655.35), kelvin: device.currentValue("colorTemperature"), duration: tt).format())
    cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    sendToDevice(cmds)
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void setColorTemperature(Number temp, Number level=null, Number transitionTime=null) {
    state.flashing = false
    List<String> cmds = []
    Integer tt = 1000
    if (transitionTime == null) {
        if (colorTransition) tt = colorTransition.toInteger() * 1000
    } else {
        tt = transitionTime * 1000
    }
    if (logEnable) log.debug "setColorTemperature(${temp}, ${level}, ${transitionTime})"
    if (level == null) level = device.currentValue("level")
    cmds.add(new hubitat.lifx.commands.SetColor(hue: Math.round(device.currentValue("hue") * 655.35), saturation: 0, brightness: Math.round(level * 655.35), kelvin: temp, duration: tt).format())
    cmds.add(new hubitat.lifx.commands.SetLightPower(level: 65535, duration: tt).format())
    sendToDevice(cmds)
    runIn((Math.round(tt/1000) + 1), privateRefresh)
}

void lifxEvent(hubitat.lifx.commands.StateWifiFirmware cmd) {
    if (logEnable) log.debug "${cmd}"
    Double fwVersion = cmd.versionMajor + (cmd.versionMinor / 100)
    device.updateDataValue("wifiFirmware", "${fwVersion}")
}

void lifxEvent(hubitat.lifx.commands.StateHostFirmware cmd) {
    if (logEnable) log.debug "${cmd}"
    Double fwVersion = cmd.versionMajor + (cmd.versionMinor / 100)
    device.updateDataValue("hostFirmware", "${fwVersion}")
}

void lifxEvent(hubitat.lifx.commands.StateVersion cmd) {
    if (logEnable) log.debug "${cmd}"
    device.updateDataValue("model: ", "${cmd.product}")
    Map<String, Object> lifxProduct = hubitat.helper.Lifx.lifxProducts[cmd.product]
    if (lifxProduct != null) {
        device.updateDataValue("modelName", lifxProduct.name)
    }
}

void lifxEvent(hubitat.lifx.commands.StateWifiInfo cmd) {
    Integer rssi = Math.floor(10 * Math.log10(cmd.signal.doubleValue()) + 0.5).toInteger()
    eventProcess(name: "rssi", value: rssi, unit: "dBm")
}

void lifxEvent(hubitat.lifx.commands.LightState cmd) {
    if (logEnable) log.debug cmd.toString()
    if (cmd.saturation > 0) {
        eventProcess(name: "colorMode", value: "RGB")
        setGenericName(Math.round(cmd.hue / 655.35))
    } else {
        eventProcess(name: "colorMode", value: "CT")
        setGenericTempName(cmd.kelvin)
    }
    eventProcess(name: "hue", value: Math.round(cmd.hue / 655.35))
    eventProcess(name: "saturation", value: Math.round(cmd.saturation / 655.35), unit: "%")
    eventProcess(name: "colorTemperature", value: cmd.kelvin, unit: "K")
    eventProcess(name: "level", value: Math.round(cmd.brightness / 655.35), unit: "%")
    if (cmd.power > 0) {
        eventProcess(name: "switch", value: "on")
    } else {
        eventProcess(name: "switch", value: "off")
    }
    eventProcess(name: "color", value: ColorUtils.rgbToHEX(ColorUtils.hsvToRGB([Math.round(cmd.hue / 655.35), Math.round(cmd.saturation / 655.35), Math.round(cmd.brightness / 655.35)])))
}

private void setGenericTempName(temp){
    if (!temp) return
    String genericName
    int value = temp.toInteger()
    if (value <= 2000) genericName = "Sodium"
    else if (value <= 2100) genericName = "Starlight"
    else if (value < 2400) genericName = "Sunrise"
    else if (value < 2800) genericName = "Incandescent"
    else if (value < 3300) genericName = "Soft White"
    else if (value < 3500) genericName = "Warm White"
    else if (value < 4150) genericName = "Moonlight"
    else if (value <= 5000) genericName = "Horizon"
    else if (value < 5500) genericName = "Daylight"
    else if (value < 6000) genericName = "Electronic"
    else if (value <= 6500) genericName = "Skylight"
    else if (value < 20000) genericName = "Polar"
    String descriptionText = "${device.getDisplayName()} color is ${genericName}"
    eventProcess(name: "colorName", value: genericName ,descriptionText: descriptionText)
}

private void setGenericName(hue){
    String colorName
    hue = hue.toInteger()
    hue = (hue * 3.6)
    switch (hue.toInteger()){
        case 0..15: colorName = "Red"
            break
        case 16..45: colorName = "Orange"
            break
        case 46..75: colorName = "Yellow"
            break
        case 76..105: colorName = "Chartreuse"
            break
        case 106..135: colorName = "Green"
            break
        case 136..165: colorName = "Spring"
            break
        case 166..195: colorName = "Cyan"
            break
        case 196..225: colorName = "Azure"
            break
        case 226..255: colorName = "Blue"
            break
        case 256..285: colorName = "Violet"
            break
        case 286..315: colorName = "Magenta"
            break
        case 316..345: colorName = "Rose"
            break
        case 346..360: colorName = "Red"
            break
    }
    String descriptionText = "${device.getDisplayName()} color is ${colorName}"
    eventProcess(name: "colorName", value: colorName ,descriptionText: descriptionText)
}

void sendToDevice(List<String> cmds, Long delay = 300) {
    sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.LIFX))
}

void sendToDevice(String cmd, Long delay = 300) {
    sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.LIFX))
}

List<String> commands(List<String> cmds, Long delay = 300) {
    return delayBetween(cmds.collect { it }, delay)
}
4 Likes

Tease!

1 Like

Should I take the release notes to mean that you haven’t had a chance to build a driver for the IR bulbs yet?

1 Like

The 1 IR bulb I have wasn’t responding to the documented IR commands..

Now .. That being said.. If you configure the IR in the LIFX app it will automatically turn to IR when the level reaches 0

1 Like

These Debug Unhandled Command messages are not indicating anything wrong, but I bet you're interested in seeing them:

Update: I can't reproduce it, so far.

1 Like

That’s right, I’m being dumb and forgot that’s how it worked. Never mind!

2 Likes

You will see those pop up with discovery messages.. I should probably filter those out before parse.

2 Likes

I'll report another behavior.. it's not wrong, but I tested it and now no one else has to :smiley:

I unplugged my one and only Lifx bulb and then from Device Info page, turned it on and off and set color, etc. All to a "dead" bulb because I wanted to know if it would jam up the hub. For a single bulb, it does not. But I think it might for a large number of Lifx devices.

The top few lines is when I plugged the Lifx bulb back in. It's the lines going back to yesterday that's the focus of this message :slight_smile: The hub's cached the command(s) and is trying them once a minute. Occasionally, multiple commands, once a minute. As I said, one is no problem, put a bunch of Lifx devices on the hub and theoretically there could be a resource issue.

4 Likes

Just to piggyback on another thread.. but are the CT mode on the color bulbs just slightly “off”, especially when compared to hue bulbs? I swapped out a hue bulb for one of the Lifx bulbs for testing purposes and the CT whites are just feel slightly red down in the 2700 range that I was using previously.

1 Like

I have noticed a difference.. but I would argue that the LIFX CT is more accurate..

1 Like

Moved this public.. I will also be updating docs.hubitat.com with all the developer related LIFX stuff..

3 Likes

Making progress here..

https://docs.hubitat.com/index.php?title=LIFX_Classes

2 Likes

@bcopeland Firstly just wanted to say thank you for making this integration officially! been waiting for it for ages and I'm so glad it's finally on Hubitat.

I have noticed some strange behaviour: I have 28 A19s - if I want to turn them on or off using the on / off control straight from the admin - I've noticed I have to double tap/click the on or off command button for it to work - any ideas on this?

1 Like

@bcopeland I am having the same issue re: double tapping that I posted here yesterday: LIFX Built-in drivers - questions and discussion - #48 by dario_rossi

Any advice would be appreciated!

Can you turn on debug logging and screenshot the logs when this happens?

Light was on at 5:26pm (started as on). I pressed off at 5:26:31.252 but light did not turn off. Waited until 5:30:30.668 and double clicked the off button and light turned off and you can see the info message that light was off. Clip of logs is below. I redacted info that looked like it could be identifiable. If you need more specific info, can we communicate outside of this thread?

@bcopeland Also worth mentioning this double tapping issue is actually more than just from the admin. If I set a button to turn on an LIFX light when it is pushed using a simple rule, it won't work (or at least 8 times out of 10 it won't work) unless the command is issued twice one after another in the rule script.

I agree with @AdamV I just used the device page as an example but the behavior is same regardless of where used: dashboard, device page, rules, etc…

Last thing from me - @bcopeland did .115 address this potential issue in trying to send commands to many devices that are off from a power source: LIFX Driver Code - #9 by csteele