Decided to scrape together code I’ve found on here to make a WLED driver that is compatible with Google Home. It is not the most optimal, and many of the advanced features haven't been tested, but the light function works (RGBCT).
I can make an installation video if there is any interest.
/**
*
* WLED Device Type
*
* rebuilt by boomking
*
* Date: 2025 22/3
*/
import java.net.URLEncoder
metadata {
definition (name: "WLED", namespace: "boomking", author: "boomking") {
capability "Color Control"
capability "Color Temperature"
capability "Refresh"
capability "Switch"
capability "Switch Level"
capability "Light"
capability "ColorMode"
capability "Alarm"
attribute "colorName", "string"
attribute "effectName", "string"
attribute "paletteName", "string"
attribute "presetValue", "Number"
//command "getEffects"
//command "getPalettes"
command "setEffect",
[
[name:"FX ID", type: "NUMBER", description: "Effect ID", constraints: []],
[name:"FX Speed", type: "NUMBER", description: "Relative Effect Speed (0-255)", constraints: []],
[name:"FX Intensity", type: "NUMBER", description: "Effect Intensity(0-255)", constraints: []],
[name:"Color Palette", type: "NUMBER", description: "Color Palette", constraints: []]
]
command "setPreset",
[
[name:"Preset", type: "NUMBER", description: "Preset Number", constraints: []],
]
}
// Preferences
preferences {
input "uri", "text", title: "URI", description: "(eg. http://[wled_ip_address])", required: true, displayDuringSetup: true
input name: "ledSegment", type: "number", title: "LED Segment", defaultValue: 0
input name: "transitionTime", type: "enum", description: "", title: "Transition time", options: [[500:"500ms"],[1000:"1s"],[1500:"1.5s"],[2000:"2s"],[5000:"5s"]], defaultValue: 1000
input name: "refreshInterval", type: "enum", description: "", title: "Refresh interval", options: [
[30: "30 Seconds"],[60:"1 Minute"],[300:"5 Minutes"],[600:"10 Minutes"],[1800:"30 Minutes"],[3600:"1 Hour"],[0:"Disabled"]],
defaultValue: 3600
input name: "powerOffParent", type: "bool", description:"Turn off segment and parent controller", title: "Power Off", defaultValue: false
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
}
private eventSend(name,verb,value,unit = ""){
String descriptionText = "${device.displayName} ${name} ${verb} ${value}${unit}"
if (txtEnable) log.info "${descriptionText}"
if (unit != "") sendEvent(name: name, value: value ,descriptionText: descriptionText, unit:unit)
else sendEvent(name: name, value: value ,descriptionText: descriptionText)
}
def initialize() {
installed()
}
def installed() {
setSchedule()
refresh()
}
def updated() {
setSchedule()
getEffects()
getPalettes()
refresh()
}
def setSchedule() {
logDebug "Setting refresh interval to ${settings.refreshInterval}s"
unschedule()
switch(settings.refreshInterval){
case "0":
unschedule()
break
case "30":
schedule("0/30 * * ? * * *", refresh)
break
case "60":
schedule("0 * * ? * * *", refresh)
break
case "300":
schedule("0 0/5 * ? * * *", refresh)
break
case "600":
schedule("0 0/10 * ? * * *", refresh)
break
case "1800":
schedule("0 0/30 * ? * * *", refresh)
break
case "3600":
schedule("0 * 0/1 ? * * *", refresh)
break
default:
unschedule()
}
}
def logsOff(){
log.warn "debug logging disabled..."
device.updateSetting("logEnable",[value:"false",type:"bool"])
}
def parseResp(resp) {
// Handle Effects and Palettes
if(!state.effects)
getEffects()
if(!state.palettes)
getPalettes()
def effects = state.effects
def palettes = state.palettes
// Update State
logDebug resp
state = resp
state.effects = effects
state.palettes = palettes
synchronize(resp)
}
// Handle async callback
def parseResp(resp, data) {
if(resp.getStatus() == 200)
parseResp(resp.getJson())
else if(resp.getStatus() == 408)
log.error "HTTP Request Timeout"
else
log.error "Unhandled HTTP Error"
}
def parsePostResp(resp){
// TODO
}
def parsePostResp(resp, data) {
if(resp.getStatus() == 200)
parsePostResp(resp.getJson())
else if(resp.getStatus() == 408)
log.error "HTTP Request Timeout"
else
log.error "Unhandled HTTP Error"
}
def synchronize(data){
logDebug "Synchronizing status: ${data.seg[settings.ledSegment?.toInteger() ?: 0]}}"
seg = data.seg[settings.ledSegment?.toInteger() ?: 0]
// Power
if(seg.on){
if(device.currentValue("switch") != "on")
sendEvent(name: "switch", value: "on")
}
else {
if(device.currentValue("switch") == "on")
sendEvent(name: "switch", value: "off")
}
//TODO: Synchronize everything else
}
// Switch Capabilities
def on() {
sendEthernetPost("/json/state","{\"on\":true, \"seg\": [{\"id\": ${ledSegment}, \"on\":true}]}")
sendEvent(name: "switch", value: "on")
}
def off() {
if(powerOffParent)
parentOff()
else
segmentOff()
}
def parentOff(){
sendEthernetPost("/json/state","{\"on\":false,\"pl\":0,\"ps\":0,\"seg\": [{\"id\": ${ledSegment}, \"on\":false}]}")
sendEvent(name: "switch", value: "off")
}
def segmentOff() {
sendEthernetPost("/json/state","{\"pl\":0,\"ps\":0,\"seg\": [{\"id\": ${ledSegment}, \"on\":false}]}")
sendEvent(name: "switch", value: "off")
}
// Color Names
def setGenericTempName(temp){
if (!temp) return
def genericName
def 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"
def descriptionText = "${device.getDisplayName()} color is ${genericName}"
if (txtEnable) log.info "${descriptionText}"
sendEvent(name: "colorName", value: genericName ,descriptionText: descriptionText)
}
def setGenericName(hue){
def colorName
hue = hue.toInteger()
if (!hiRezHue) 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
}
def descriptionText = "${device.getDisplayName()} color is ${colorName}"
if (txtEnable) log.info "${descriptionText}"
sendEvent(name: "colorName", value: colorName ,descriptionText: descriptionText)
}
// Dimmer function
def setLevel(value) {
setLevel(value, (transitionTime?.toBigDecimal() ?: 1000) / 1000)
}
def setLevel(value, rate) {
// TODO: implement transition rate
if(value > 0){
def isOn = device.currentValue("switch") == "on"
if(!isOn)
on()
if(value >= 100) {
setValue = 255
value = 100
}
else {
setValue = (value.toInteger() * 2.55).toInteger()
}
msg = "{\"on\":true, \"seg\": [{\"id\": ${ledSegment}, \"on\":true, \"bri\": ${setValue}}]}"
sendEthernetPost("/json/state", msg)
sendEvent(name: "level", value: value, descriptionText: "${device.displayName} is ${value}%", unit: "%")
} else {
off()
}
refresh()
}
// Color Functions
def setColor(value){
if (value.hue == null || value.saturation == null) return
def rate = transitionTime?.toInteger() ?: 1000
// Turn off if level is set to 0/black
if (value.level == 0) {
off()
return
} else if(value.level >= 100) {
level = 255
} else {
level = value.level * 256
}
// Convert to RGB from HSV
rgbValue = hsvToRgb(value.hue, value.saturation, value.level)
eventSend("saturation",verb,value.saturation,"%")
eventSend("hue",verb,value.hue,"%")
// Send to WLED
logDebug("Setting RGB Color to ${rgbValue}")
setRgbColor(rgbValue)
setGenericName(value.hue)
if (device.currentValue("colorMode") != "RGB") {
eventSend("colorMode","is","RGB")
}}
def setColorTemperature(temp, level, transitionTime){
if (level) {
setLevel(level, transitionTime)
}
eventSend("colorTemperature",verb,temp,"°K")
setColorTemperature(temp)
}
def setColorTemperature(temp, level){
if (level) {
setLevel(level)
}
setColorTemperature(temp)
}
def setColorTemperature(temp){
on()
rgbValue = colorTempToRgb(temp)
setRgbColor(rgbValue)
setGenericTempName(temp)
if (device.currentValue("colorMode") != "CT") {
eventSend("colorMode","is","CT")
}}
def setHue(value){
def color = [:]
color.hue = value
color.level = device.currentValue("level") ?: 100 // Use current level or default to 100
color.saturation = device.currentValue("saturation") ?: 100 // Default to full saturation
setColor(color)
}
def setRgbColor(rgbValue){
// Turn off any active effects
setEffect(0,0)
// Send Color
body = "{\"on\":true, \"seg\": [{\"id\": ${ledSegment}, \"on\":true, \"col\": [${rgbValue}]}]}"
logDebug("Setting color: ${body}")
sendEthernetPost("/json/state", body)
refresh()
}
// Device Functions
def refresh() {
sendEthernet("/json/state")
}
def sendEthernet(path) {
if(settings.uri != null){
def params = [
uri: "${settings.uri}",
path: "${path}",
requestContentType: 'application/json',
contentType: 'application/json',
headers: [:],
timeout: 5
]
try {
asynchttpGet('parseResp',params)
} catch (e) {
log.error "something went wrong: $e"
}
}
}
def sendEthernetPost(path, body) {
if(settings.uri != null){
def params = [
uri: "${settings.uri}",
path: "${path}",
requestContentType: 'application/json',
contentType: 'application/json',
body: "${body}",
timeout: 5
]
try {
asynchttpPut(null, params)
} catch (e) {
log.error "something went wrong: $e"
}
}
}
// Helper Functions
def logDebug(message){
if(logEnable) log.debug(message)
}
def hsvToRgb(float hue, float saturation, float value) {
if(hue==100) hue = 99
hue = hue/100
saturation = saturation/100
value = value/100
int h = (int)(hue * 6)
float f = hue * 6 - h
float p = value * (1 - saturation)
float q = value * (1 - f * saturation)
float t = value * (1 - (1 - f) * saturation)
switch (h) {
case 0: return rgbToString(value, t, p)
case 1: return rgbToString(q, value, p)
case 2: return rgbToString(p, value, t)
case 3: return rgbToString(p, q, value)
case 4: return rgbToString(t, p, value)
case 5: return rgbToString(value, p, q)
default: log.error "Something went wrong when converting from HSV to RGB. Input was " + hue + ", " + saturation + ", " + value
}
}
def colorTempToRgb(kelvin){
temp = kelvin/100
if( temp <= 66 ){
red = 255
green = temp
green = 99.4708025861 * Math.log(green) - 161.1195681661
if( temp <= 19){
blue = 0
} else {
blue = temp-10
blue = 138.5177312231 * Math.log(blue) - 305.0447927307
}
} else {
red = temp - 60
red = 329.698727446 * Math.pow(red, -0.1332047592)
green = temp - 60
green = 288.1221695283 * Math.pow(green, -0.0755148492 )
blue = 255
}
rs = clamp(red,0, 255)
gs = clamp(green,0,255)
bs = clamp(blue,0, 255)
return "[" + rs + "," + gs + "," + bs + "]";
}
def rgbToString(float r, float g, float b) {
String rs = (int)(r * 255)
String gs = (int)(g * 255)
String bs = (int)(b * 255)
return "[" + rs + "," + gs + "," + bs + "]";
}
def clamp( x, min, max ) {
if(x<min){ return min; }
if(x>max){ return max; }
return x;
}
// FastLED FX and Palletes
def getEffects(){
logDebug "Getting Effects List"
def params = [
uri: "${settings.uri}",
path: "/json/effects",
headers: [
"Content-Type": "application/json",
"Accept": "*/*"
],
body: "${body}",
timeout: 5
]
try {
httpGet(params) {
resp ->
state.effects = resp.data
}
} catch (e) {
log.error "something went wrong: $e"
}
}
def getPalettes(){
logDebug "Getting Palettes List"
def params = [
uri: "${settings.uri}",
path: "/json/palettes",
headers: [
"Content-Type": "application/json",
"Accept": "*/*"
],
body: "${body}",
timeout: 5
]
try {
httpGet(params) {
resp ->
state.palettes = resp.data
}
} catch (e) {
log.error "something went wrong: $e"
}
}
def setEffect(fx){
def i = ledSegment?.toInteger() ?: 0
setEffect(fx, state.seg[i].sx, state.seg[i].ix, state.seg[i].pal)
}
def setEffect(fx, pal){
def i = ledSegment?.toInteger() ?: 0
setEffect(fx, state.seg[i].sx, state.seg[i].ix, pal)
}
def setEffect(fx,sx,ix){
def i = ledSegment?.toInteger() ?: 0
setEffect(fx, sx, ix, state.seg[i].pal)
}
def setEffect(fx, sx, ix, pal){
logDebug("Setting Effect: [{\"id\": ${ledSegment},\"fx\": ${fx},\"sx\": ${sx},\"ix\": ${ix},\"pal\": ${pal}}]")
body = "{\"on\":true, \"seg\": [{\"id\": ${ledSegment},\"fx\": ${fx},\"sx\": ${sx},\"ix\": ${ix},\"pal\": ${pal}}]}"
sendEthernetPost("/json/state", body)
// Effect Name
def effectName = state.effects.getAt(fx.intValue())
def descriptionText = "${device.getDisplayName()} effect is ${effectName}"
if (txtEnable) log.info "${descriptionText}"
sendEvent(name: "effectName", value: effectName, descriptionText: descriptionText)
// Palette Name
def paletteName = state.palettes.getAt(pal.intValue())
descriptionText = "${device.getDisplayName()} color palette is ${paletteName}"
if (txtEnable) log.info "${descriptionText}"
sendEvent(name: "paletteName", value: paletteName, descriptionText: descriptionText)
if(fx > 0){
// Color Name
descriptionText = "${device.getDisplayName()} color is defined by palette"
sendEvent(name: "colorName", value: "Palette", descriptionText: descriptionText)
}
// Refresh
refresh()
}
def setEffectCustom(fx, sx, ix, pal){
// support for webCORE
setEffect(fx, sx, ix, pal)
}
def setPreset(preset)
{
logDebug("${device.getDisplayName()} setting preset to ${preset}")
msg = "{\"on\":true, \"ps\": ${preset}}"
sendEthernetPost("/json/state", msg)
sendEvent(name: "presetValue", value: preset, descriptionText: "${device.displayName} preset is set to ${preset}")
}
// Alarm Functions
def siren(){
// Play "Siren" effect
logDebug("Alarm \"siren\" activated")
setEffect(38,255,255,0)
}
def strobe(){
// Set Effect to Strobe
logDebug("Alarm strobe activated")
setEffect(23,255,255,0)
}
def both(){
//Cannot do both, default to strobe
strobe()
}