First of all, thank you very much for your input! That was super useful and got me on the right track. And sorry for the delay. It took me a lot of time to actually set it up the way I wanted to.
Mostly because I took it further than original proposed.
I really liked your solution with the RESTful Switch, and it worked perfectly to turn segments on or off.
However, there were some issues with it:
- You had to specifiy the color it is turned on with in advance… It couldn’t just turn on the last color it showed.
- The on/off switch was seperate to the light entities that controlled the color, brightness and so on
- The on/off switch of the light entity would still turn off the entire led strip, not just the segment
- You couldn’t set the brightness of each segment individually
Therefore I digged a little bit deeper and build a solution that solves all of them. But before I go into how that looked like some notes first:
First of all, I do hope that no one has to go through rebuilding my solution as the next version (0.9.2) of Aircookies WLED Webserver should support the brightness and on property for each segment natively.
As soon as that is available I will also switch back to the default WLED integration myself.
Furthemore, the solution I build is not optimal in the least… it’s just what I came up with after a lot of trial and error. I’ll point out what or how it could be done better in the end.
Now let’s get to it:
To overcome the first issue, you can just delete the segment you want to turn off by setting the “stop” parameter of that segment to 0 when using the JSON API and posting a new state.
To turn it back on, you only need to re-add the segment by posting a new state with the old “stop” value of that segment.
I implemented that using a Node-Red Flow that you can copy paste from here:
[{"id":"2ef0c868.ebb428","type":"http in","z":"748593bf.043ebc","name":"Segment Set On/Off","url":"/wled-segments/power/:seg_id/:state","method":"get","upload":false,"swaggerDoc":"","x":190,"y":580,"wires":[["5bfac212.f6bd0c","8d25993e.863f98"]]},{"id":"5bfac212.f6bd0c","type":"function","z":"748593bf.043ebc","name":"buildOnOffJson","func":"seg_id = msg.req.params.seg_id\nstate = msg.req.params.state\n\nconst DEFAULT_SEGMENTS =[\n \n {\n \"id\": 0,\n \"start\": 0,\n \"stop\": 448\n },\n {\n \"id\": 1,\n \"start\": 0,\n \"stop\": 52\n },\n {\n \"id\": 2,\n \"start\": 52,\n \"stop\": 100\n },\n {\n \"id\": 3,\n \"start\": 100,\n \"stop\": 190\n },\n {\n \"id\": 5,\n \"start\": 243,\n \"stop\": 290\n },\n {\n \"id\": 4,\n \"start\": 190,\n \"stop\": 243\n },\n {\n \"id\": 6,\n \"start\": 290,\n \"stop\": 332\n },\n {\n \"id\": 7,\n \"start\": 332,\n \"stop\": 386\n },\n {\n \"id\": 8,\n \"start\": 386,\n \"stop\": 448\n }\n ]\n \nseg = DEFAULT_SEGMENTS.filter((s) => s.id == seg_id)[0]\nif (state == \"on\"){\n msg.payload = {\"on\": true, \"seg\":[seg]}\n}\nelse if (state == \"off\"){\n seg.stop = 0\n msg.payload = {\"on\": true, \"seg\":[seg]}\n}\nelse{\n msg.payload = {}\n}\n \n \n\nreturn msg;","outputs":1,"noerr":0,"x":420,"y":580,"wires":[["201e58e7.9d47b8","c5285d78.f30b2"]]},{"id":"201e58e7.9d47b8","type":"http request","z":"748593bf.043ebc","name":"","method":"POST","ret":"obj","paytoqs":false,"url":"http://192.168.178.119/json/state","tls":"","persist":false,"proxy":"","authType":"","x":650,"y":580,"wires":[["d56a875d.46d618"]]},{"id":"d56a875d.46d618","type":"http response","z":"748593bf.043ebc","name":"","statusCode":"","headers":{"content-type":"application/json"},"x":870,"y":580,"wires":[]},{"id":"c5285d78.f30b2","type":"debug","z":"748593bf.043ebc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":630,"y":640,"wires":[]},{"id":"8d25993e.863f98","type":"debug","z":"748593bf.043ebc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":430,"y":640,"wires":[]}]
Note: For this to work properly your “main segment”, usually the segment 0, should not be used as a light in HA. Set its color to black by default and never touch it again. For every part of your LED strip define a segment to control it with. Otherwise, when you turn off a segment by deleting it, this part of the strip will take the color of the main segment. Also, if you don’t do it this way, you’ll never be able to “turn of” the last segment, as you can’t delete the main segment.
Tipp: Aircookies WLED allows you to set presets. The preset 16 is the only one that can save the configuration of your segment setups. Use it and in your local WLED controll website go to “config/LED Perferences” and set “Apply preset ## at boot” to 16. Also make sure that this preset has segment 0 set to black and brightness to max.
The last three issues I solved by creating my own light template, sensors templates, rest commands and corresponding Node-Red flows… Probably a total overkill:
For every segment I created a sensor like this:
sensor:
- platform: rest
name: led_ceiling_office_front
resource: http://homeassistant:1880/endpoint/wled-segments/state/1
value_template: '{{value_json["on"]}}'
json_attributes:
- transition
- id
- start
- stop
- len
- grp
- rgb_color
- hs_color
- level
- temperature
username: "<your node-red user>"
password: "<your node-red password>"
# update every second
scan_interval: 1
And the corresponding light template
light:
- platform: template
lights:
ceiling_office_front:
friendly_name: "LED Office Front"
entity_id:
- sensor.led_ceiling_office_front
value_template: "{{states('sensor.led_ceiling_office_front')}}"
level_template: "{{state_attr('sensor.led_ceiling_office_front', 'level')| int}}"
temperature_template: "{{state_attr('sensor.led_ceiling_office_front', 'temperature')| int}}"
color_template: >
{% set hsl_col = state_attr('sensor.led_ceiling_office_front', 'hs_color') %}
({{hsl_col[0]| int}}, {{hsl_col[1]| int}})
turn_on:
service: rest_command.wled_segment_power
data:
id: 1
value: "on"
turn_off:
service: rest_command.wled_segment_power
data:
id: 1
value: "off"
set_level:
service: rest_command.wled_segment_color
data_template:
id: 1
hs_color: "{{state_attr('sensor.led_ceiling_office_front', 'hs_color')}}"
level: "{{brightness}}"
set_color:
service: rest_command.wled_segment_color
data_template:
id: 1
hs_color: "{{[h,s]}}"
level: "{{state_attr('sensor.led_ceiling_office_front', 'level')}}"
set_temperature:
service: rest_command.wled_segment_color
data_template:
id: 1
temperature: "{{color_temp}}"
level: "{{state_attr('sensor.led_ceiling_office_front', 'level')}}"
The rest commands I used looked like this:
rest_command:
wled_segment_color:
method: POST
url: http://homeassistant:1880/endpoint/wled-segments/color/
content_type: 'application/json; charset=utf-8'
username: "<your node-red user>"
password: "<your node-red password>"
payload: >
{"id":{{id}}, "hs_color":{{hs_color|default('"undefined"', true)}}, "temperature":{{temperature|default('"undefined"', true)}}, "level":{{level}}}
wled_segment_power:
method: GET
url: "http://homeassistant:1880/endpoint/wled-segments/power/{{id}}/{{value}}"
content_type: 'application/json; charset=utf-8'
username: "<your node-red user>"
password: "<your node-red password>"
And finally the node red flow to get the state:
[{"id":"9f70d7a7.fe4d48","type":"http in","z":"748593bf.043ebc","name":"Segment State","url":"/wled-segments/state/:seg_num","method":"get","upload":false,"swaggerDoc":"","x":160,"y":80,"wires":[["1858d4ce.be16eb","7da15303.9a1d3c"]]},{"id":"635a25e8.7154fc","type":"http response","z":"748593bf.043ebc","name":"","statusCode":"","headers":{"content-type":"application/json"},"x":870,"y":80,"wires":[]},{"id":"7da15303.9a1d3c","type":"http request","z":"748593bf.043ebc","name":"","method":"GET","ret":"obj","paytoqs":false,"url":"http://192.168.178.119/json/state","tls":"","persist":false,"proxy":"","authType":"","x":410,"y":80,"wires":[["b9e3b453.7f03d8","7000be03.07044"]]},{"id":"b9e3b453.7f03d8","type":"function","z":"748593bf.043ebc","name":"flattenJson","func":"wled_payload = msg.payload\nseg_id = msg.req.params.seg_num\nif (seg_id == null){\n seg_id = 0\n}\n\n// Kelvin temperature bounds\nconst KELVIN_MIN = 2000;\nconst KELVIN_MAX = 7000;\n\n// Math shorthands\nconst { log, round, floor } = Math;\n\nfunction mired_to_kelvin(mired){\n return Math.round(1000000/mired)\n}\n\nfunction kelvin_to_mired (kelvin){\n return Math.round(1000000/kelvin)\n}\n\nfunction kelvinToRgb(kelvin) {\n const temp = kelvin / 100;\n let r, g, b;\n if (temp < 66) {\n r = 255\n g = -155.25485562709179 - 0.44596950469579133 * (g = temp-2) + 104.49216199393888 * log(g)\n b = temp < 20 ? 0 : -254.76935184120902 + 0.8274096064007395 * (b = temp-10) + 115.67994401066147 * log(b)\n } else {\n r = 351.97690566805693 + 0.114206453784165 * (r = temp-55) - 40.25366309332127 * log(r)\n g = 325.4494125711974 + 0.07943456536662342 * (g = temp-50) - 28.0852963507957 * log(g)\n b = 255\n }\n return {r: floor(r), g: floor(g), b: floor(b)};\n}\n\nfunction rgbToKelvin(rgb){\n const r = rgb[0];\n const g = rgb[1];\n const b = rgb[2];\n const eps = 0.4;\n let minTemp = KELVIN_MIN;\n let maxTemp = KELVIN_MAX;\n let temp;\n while (maxTemp - minTemp > eps) {\n temp = (maxTemp + minTemp) * 0.5;\n const rgb = kelvinToRgb(temp);\n if ((rgb.b / rgb.r) >= (b / r)) {\n maxTemp = temp;\n } else {\n minTemp = temp;\n }\n }\n return temp;\n }\nfunction rgbToHsv(rgb) {\n const r = rgb[0] / 255;\n const g = rgb[1] / 255;\n const b = rgb[2] / 255;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const delta = max - min;\n let hue = 0;\n let value = max;\n let saturation = max === 0 ? 0 : delta / max;\n switch (max) {\n case min: \n hue = 0; // achromatic\n break;\n case r: \n hue = (g - b) / delta + (g < b ? 6 : 0);\n break;\n case g: \n hue = (b - r) / delta + 2;\n break;\n case b:\n hue = (r - g) / delta + 4;\n break;\n }\n return {\n h: hue * 60,\n s: saturation * 100,\n v: value * 100\n }\n }\n\n\nmsg.payload = {\"on\": wled_payload.on}\n\nif (msg.payload.on){\n wled_seg = wled_payload.seg.filter((seg) => seg.id == seg_id)[0]\n msg.wled_seg = wled_seg\n if (wled_seg != undefined){\n hsv = rgbToHsv(wled_seg.col[0])\n msg.payload = {\n \"on\": true,\n \"transition\": wled_payload.transition,\n \"id\": wled_seg.id,\n \"start\": wled_seg.start,\n \"stop\": wled_seg.stop,\n \"len\": wled_seg.len,\n \"grp\": wled_seg.grp,\n \"rgb_color\": wled_seg.col[0],\n \"hs_color\": [hsv.h, hsv.s]\n }\n \n msg.payload.temperature = kelvin_to_mired(rgbToKelvin(wled_seg.col[0]))\n msg.payload.level = hsv.v / 100 * 255\n msg.payload.level = Math.round(msg.payload.level)\n msg.payload.temperature = Math.round(msg.payload.temperature)\n \n }\n else{\n msg.payload = {\"on\": false}\n }\n}\n\n\n\nmsg.seg_id = seg_id\nreturn msg;","outputs":1,"noerr":0,"x":630,"y":80,"wires":[["27ca915e.bfe99e","635a25e8.7154fc"]]}]
This flow just calls the WLED json/state API, flattens the payload to be able to extract it nicely using the sensor template and most importantly does some conversions and calculations on the rgb values that I stole from this nice gentleman.
From the RGB I calculate the HSV and color temperture and used the “value” value as the brightness of an individual segment.
And here the flow to change the color:
[{"id":"cda494a8.765378","type":"http in","z":"748593bf.043ebc","name":"Segment Set Color","url":"/wled-segments/color/","method":"post","upload":false,"swaggerDoc":"","x":170,"y":360,"wires":[["d2ced552.d29718","a8fe5976.8e3828"]]},{"id":"a8fe5976.8e3828","type":"function","z":"748593bf.043ebc","name":"buildColorJson","func":"ha_payload = msg.payload\n\n// Kelvin temperature bounds\nconst KELVIN_MIN = 1000;\nconst KELVIN_MAX = 40000;\n\n// Math shorthands\nconst { log, round, floor } = Math;\n\nfunction mired_to_kelvin(mired){\n return Math.round(1000000/mired)\n}\n\nfunction adjust_brightness (rgb, level){\n relative_level = level/255\n return{r: floor(rgb.r* relative_level), g: floor(rgb.g* relative_level), b: floor(rgb.b* relative_level)}\n}\n\nfunction kelvinToRgb(kelvin) {\n const temp = kelvin / 100;\n let r, g, b;\n if (temp < 66) {\n r = 255\n g = -155.25485562709179 - 0.44596950469579133 * (g = temp-2) + 104.49216199393888 * log(g)\n b = temp < 20 ? 0 : -254.76935184120902 + 0.8274096064007395 * (b = temp-10) + 115.67994401066147 * log(b)\n } else {\n r = 351.97690566805693 + 0.114206453784165 * (r = temp-55) - 40.25366309332127 * log(r)\n g = 325.4494125711974 + 0.07943456536662342 * (g = temp-50) - 28.0852963507957 * log(g)\n b = 255\n }\n return {r: floor(r), g: floor(g), b: floor(b)};\n}\n\nfunction hsvToRgb(h, s, v) {\n h = h / 60;\n s = s / 100;\n v = v / 100;\n const i = floor(h);\n const f = h - i;\n const p = v * (1 - s);\n const q = v * (1 - f * s);\n const t = v * (1 - (1 - f) * s);\n const mod = i % 6;\n const r = [v, q, p, p, t, v][mod];\n const g = [t, v, v, q, p, p][mod];\n const b = [p, p, t, v, v, q][mod];\n return {\n r: r * 255, \n g: g * 255, \n b: b * 255\n };\n }\n\n\nif (ha_payload.id != undefined){\n msg.payload = {\"on\": true, \"seg\":[{\"id\":ha_payload.id}]}\n \n if (ha_payload.hs_color == \"undefined\" \n && ha_payload.temperature != \"undefined\"){\n \n rgb = kelvinToRgb(mired_to_kelvin(ha_payload.temperature))\n rgb = adjust_brightness(rgb, ha_payload.level)\n }\n else{\n h = ha_payload.hs_color[0]\n s = ha_payload.hs_color[1]\n v = ha_payload.level / 255 * 100\n \n //if (s == 0) //pure white\n // l = ha_payload.level / 255 * 100\n //else\n // l = ha_payload.level / 255 * 50\n rgb = hsvToRgb(h, s, v)\n }\n msg.payload.seg[0].col = [[rgb.r, rgb.g, rgb.b]]\n}\nelse{\n msg.statusCode = 404\n msg.payload = {\"error\": \"segment with id \" + seg_id + \" not found\"}\n}\n\n\nmsg.seg_id = ha_payload.id\nreturn msg;","outputs":1,"noerr":0,"x":420,"y":360,"wires":[["37dd60bc.d849f","30bec473.d92bcc"]]},{"id":"37dd60bc.d849f","type":"http request","z":"748593bf.043ebc","name":"","method":"POST","ret":"obj","paytoqs":false,"url":"http://192.168.178.119/json/state","tls":"","persist":false,"proxy":"","authType":"","x":610,"y":360,"wires":[["4f7c738.a46af8c","4691a912.95aea8"]]},{"id":"4f7c738.a46af8c","type":"http response","z":"748593bf.043ebc","name":"","statusCode":"","headers":{"content-type":"application/json"},"x":810,"y":360,"wires":[]}]
This last flow provides the REST endpoint to change the color, either based on a color temperature or the hue and saturation. Brightness/Value/Level parameter is always expected.
Having all of this in place, enables you to create light entities for individual segments that are fully functional.
But it’s not perfect… Mostly because the lights are not as responsive as other lights that are added with the built-in integrations. You might be able to reduce this by setting the scan_interval of the sensors to 0.5 or something like that. But this will put more stress on your device (probably ESP32 or ESP8622) that is hosting the WLED server controlling your LEDs. Especially if you have lots of segments.
Also I noticed when grouping multiple segments together to be controlled by one light entity, they are not synchronised perfectly…
Alternative Solution:
Instead of building a node red flow and using sensors to get the state and set the color, you could also just go back to the REST Switch, built yourself a light template and base it on the entities created by the WLED HA integration. Using this as a replacement for the sensors. However, you will lose the ability to change the brightness of lights individually.