Add CCT support to WLED (#122488)

This commit is contained in:
Stefano Semeraro 2024-07-24 20:37:38 +02:00 committed by GitHub
parent d7c713d18d
commit 34b32ced25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 451 additions and 2 deletions

View File

@ -19,6 +19,7 @@ CONF_KEEP_MAIN_LIGHT = "keep_master_light"
DEFAULT_KEEP_MAIN_LIGHT = False
# Attributes
ATTR_CCT = "cct"
ATTR_COLOR_PRIMARY = "color_primary"
ATTR_DURATION = "duration"
ATTR_FADE = "fade"
@ -30,6 +31,10 @@ ATTR_SPEED = "speed"
ATTR_TARGET_BRIGHTNESS = "target_brightness"
ATTR_UDP_PORT = "udp_port"
# Static values
COLOR_TEMP_K_MIN = 2000
COLOR_TEMP_K_MAX = 6535
LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] = {
LightCapability.NONE: [
@ -56,8 +61,8 @@ LIGHT_CAPABILITIES_COLOR_MODE_MAPPING: dict[LightCapability, list[ColorMode]] =
LightCapability.RGB_COLOR
| LightCapability.WHITE_CHANNEL
| LightCapability.COLOR_TEMPERATURE: [
ColorMode.RGB,
ColorMode.COLOR_TEMP,
ColorMode.RGBW,
],
LightCapability.MANUAL_WHITE: [
ColorMode.BRIGHTNESS,

View File

@ -35,3 +35,13 @@ def wled_exception_handler[_WLEDEntityT: WLEDEntity, **_P](
raise HomeAssistantError("Invalid response from WLED API") from error
return handler
def kelvin_to_255(k: int, min_k: int, max_k: int) -> int:
"""Map color temperature in K from minK-maxK to 0-255."""
return int((k - min_k) / (max_k - min_k) * 255)
def kelvin_to_255_reverse(v: int, min_k: int, max_k: int) -> int:
"""Map color temperature from 0-255 to minK-maxK K."""
return int(v / 255 * (max_k - min_k) + min_k)

View File

@ -7,6 +7,7 @@ from typing import Any, cast
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
@ -20,14 +21,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import WLEDConfigEntry
from .const import (
ATTR_CCT,
ATTR_COLOR_PRIMARY,
ATTR_ON,
ATTR_SEGMENT_ID,
COLOR_TEMP_K_MAX,
COLOR_TEMP_K_MIN,
LIGHT_CAPABILITIES_COLOR_MODE_MAPPING,
)
from .coordinator import WLEDDataUpdateCoordinator
from .entity import WLEDEntity
from .helpers import wled_exception_handler
from .helpers import kelvin_to_255, kelvin_to_255_reverse, wled_exception_handler
PARALLEL_UPDATES = 1
@ -109,6 +113,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
_attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION
_attr_translation_key = "segment"
_attr_min_color_temp_kelvin = COLOR_TEMP_K_MIN
_attr_max_color_temp_kelvin = COLOR_TEMP_K_MAX
def __init__(
self,
@ -166,6 +172,12 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
return None
return cast(tuple[int, int, int, int], color.primary)
@property
def color_temp_kelvin(self) -> int | None:
"""Return the CT color value in K."""
cct = self.coordinator.data.state.segments[self._segment].cct
return kelvin_to_255_reverse(cct, COLOR_TEMP_K_MIN, COLOR_TEMP_K_MAX)
@property
def effect(self) -> str | None:
"""Return the current effect of the light."""
@ -235,6 +247,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
if ATTR_RGBW_COLOR in kwargs:
data[ATTR_COLOR_PRIMARY] = kwargs[ATTR_RGBW_COLOR]
if ATTR_COLOR_TEMP_KELVIN in kwargs:
data[ATTR_CCT] = kelvin_to_255(
kwargs[ATTR_COLOR_TEMP_KELVIN], COLOR_TEMP_K_MIN, COLOR_TEMP_K_MAX
)
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)

View File

@ -0,0 +1,383 @@
{
"state": {
"on": true,
"bri": 255,
"transition": 7,
"ps": 2,
"pl": -1,
"nl": {
"on": false,
"dur": 60,
"mode": 1,
"tbri": 0,
"rem": -1
},
"udpn": {
"send": false,
"recv": true,
"sgrp": 1,
"rgrp": 1
},
"lor": 0,
"mainseg": 0,
"seg": [
{
"id": 0,
"start": 0,
"stop": 178,
"len": 178,
"grp": 1,
"spc": 0,
"of": 0,
"on": true,
"frz": false,
"bri": 255,
"cct": 53,
"set": 0,
"col": [
[0, 0, 0, 255],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
"fx": 0,
"sx": 128,
"ix": 128,
"pal": 0,
"c1": 128,
"c2": 128,
"c3": 16,
"sel": true,
"rev": false,
"mi": false,
"o1": false,
"o2": false,
"o3": false,
"si": 0,
"m12": 0
}
]
},
"info": {
"ver": "0.15.0-b3",
"vid": 2405180,
"cn": "Kōsen",
"release": "ESP32",
"leds": {
"count": 178,
"pwr": 0,
"fps": 0,
"maxpwr": 0,
"maxseg": 32,
"bootps": 1,
"seglc": [7],
"lc": 7,
"rgbw": true,
"wv": 2,
"cct": 4
},
"str": false,
"name": "WLED CCT light",
"udpport": 21324,
"simplifiedui": false,
"live": false,
"liveseg": -1,
"lm": "",
"lip": "",
"ws": 1,
"fxcount": 187,
"palcount": 75,
"cpalcount": 4,
"maps": [
{
"id": 0
}
],
"wifi": {
"bssid": "AA:AA:AA:AA:AA:BB",
"rssi": -44,
"signal": 100,
"channel": 11
},
"fs": {
"u": 20,
"t": 983,
"pmt": 1721752272
},
"ndc": 1,
"arch": "esp32",
"core": "v3.3.6-16-gcc5440f6a2",
"clock": 240,
"flash": 4,
"lwip": 0,
"freeheap": 164804,
"uptime": 79769,
"time": "2024-7-24, 14:34:00",
"opt": 79,
"brand": "WLED",
"product": "FOSS",
"mac": "aabbccddeeff",
"ip": "127.0.0.1"
},
"effects": [
"Solid",
"Blink",
"Breathe",
"Wipe",
"Wipe Random",
"Random Colors",
"Sweep",
"Dynamic",
"Colorloop",
"Rainbow",
"Scan",
"Scan Dual",
"Fade",
"Theater",
"Theater Rainbow",
"Running",
"Saw",
"Twinkle",
"Dissolve",
"Dissolve Rnd",
"Sparkle",
"Sparkle Dark",
"Sparkle+",
"Strobe",
"Strobe Rainbow",
"Strobe Mega",
"Blink Rainbow",
"Android",
"Chase",
"Chase Random",
"Chase Rainbow",
"Chase Flash",
"Chase Flash Rnd",
"Rainbow Runner",
"Colorful",
"Traffic Light",
"Sweep Random",
"Chase 2",
"Aurora",
"Stream",
"Scanner",
"Lighthouse",
"Fireworks",
"Rain",
"Tetrix",
"Fire Flicker",
"Gradient",
"Loading",
"Rolling Balls",
"Fairy",
"Two Dots",
"Fairytwinkle",
"Running Dual",
"RSVD",
"Chase 3",
"Tri Wipe",
"Tri Fade",
"Lightning",
"ICU",
"Multi Comet",
"Scanner Dual",
"Stream 2",
"Oscillate",
"Pride 2015",
"Juggle",
"Palette",
"Fire 2012",
"Colorwaves",
"Bpm",
"Fill Noise",
"Noise 1",
"Noise 2",
"Noise 3",
"Noise 4",
"Colortwinkles",
"Lake",
"Meteor",
"Meteor Smooth",
"Railway",
"Ripple",
"Twinklefox",
"Twinklecat",
"Halloween Eyes",
"Solid Pattern",
"Solid Pattern Tri",
"Spots",
"Spots Fade",
"Glitter",
"Candle",
"Fireworks Starburst",
"Fireworks 1D",
"Bouncing Balls",
"Sinelon",
"Sinelon Dual",
"Sinelon Rainbow",
"Popcorn",
"Drip",
"Plasma",
"Percent",
"Ripple Rainbow",
"Heartbeat",
"Pacifica",
"Candle Multi",
"Solid Glitter",
"Sunrise",
"Phased",
"Twinkleup",
"Noise Pal",
"Sine",
"Phased Noise",
"Flow",
"Chunchun",
"Dancing Shadows",
"Washing Machine",
"Rotozoomer",
"Blends",
"TV Simulator",
"Dynamic Smooth",
"Spaceships",
"Crazy Bees",
"Ghost Rider",
"Blobs",
"Scrolling Text",
"Drift Rose",
"Distortion Waves",
"Soap",
"Octopus",
"Waving Cell",
"Pixels",
"Pixelwave",
"Juggles",
"Matripix",
"Gravimeter",
"Plasmoid",
"Puddles",
"Midnoise",
"Noisemeter",
"Freqwave",
"Freqmatrix",
"GEQ",
"Waterfall",
"Freqpixels",
"RSVD",
"Noisefire",
"Puddlepeak",
"Noisemove",
"Noise2D",
"Perlin Move",
"Ripple Peak",
"Firenoise",
"Squared Swirl",
"RSVD",
"DNA",
"Matrix",
"Metaballs",
"Freqmap",
"Gravcenter",
"Gravcentric",
"Gravfreq",
"DJ Light",
"Funky Plank",
"RSVD",
"Pulser",
"Blurz",
"Drift",
"Waverly",
"Sun Radiation",
"Colored Bursts",
"Julia",
"RSVD",
"RSVD",
"RSVD",
"Game Of Life",
"Tartan",
"Polar Lights",
"Swirl",
"Lissajous",
"Frizzles",
"Plasma Ball",
"Flow Stripe",
"Hiphotic",
"Sindots",
"DNA Spiral",
"Black Hole",
"Wavesins",
"Rocktaves",
"Akemi"
],
"palettes": [
"Default",
"* Random Cycle",
"* Color 1",
"* Colors 1&2",
"* Color Gradient",
"* Colors Only",
"Party",
"Cloud",
"Lava",
"Ocean",
"Forest",
"Rainbow",
"Rainbow Bands",
"Sunset",
"Rivendell",
"Breeze",
"Red & Blue",
"Yellowout",
"Analogous",
"Splash",
"Pastel",
"Sunset 2",
"Beach",
"Vintage",
"Departure",
"Landscape",
"Beech",
"Sherbet",
"Hult",
"Hult 64",
"Drywet",
"Jul",
"Grintage",
"Rewhi",
"Tertiary",
"Fire",
"Icefire",
"Cyane",
"Light Pink",
"Autumn",
"Magenta",
"Magred",
"Yelmag",
"Yelblu",
"Orange & Teal",
"Tiamat",
"April Night",
"Orangery",
"C9",
"Sakura",
"Aurora",
"Atlantica",
"C9 2",
"C9 New",
"Temperature",
"Aurora 2",
"Retro Clown",
"Candy",
"Toxy Reaf",
"Fairy Reaf",
"Semi Blue",
"Pink Candy",
"Red Reaf",
"Aqua Flash",
"Yelblu Hot",
"Lite Light",
"Red Flash",
"Blink Red",
"Red Shift",
"Red Tide",
"Candy2"
]
}

View File

@ -9,8 +9,11 @@ from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_HS_COLOR,
ATTR_MAX_COLOR_TEMP_KELVIN,
ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
@ -374,3 +377,34 @@ async def test_single_segment_with_keep_main_light(
assert (state := hass.states.get("light.wled_rgb_light_main"))
assert state.state == STATE_ON
@pytest.mark.parametrize("device_fixture", ["cct"])
async def test_cct_light(hass: HomeAssistant, mock_wled: MagicMock) -> None:
"""Test CCT support for WLED."""
assert (state := hass.states.get("light.wled_cct_light"))
assert state.state == STATE_ON
assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [
ColorMode.COLOR_TEMP,
ColorMode.RGBW,
]
assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.COLOR_TEMP
assert state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN) == 2000
assert state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN) == 6535
assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) == 2942
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.wled_cct_light",
ATTR_COLOR_TEMP_KELVIN: 4321,
},
blocking=True,
)
assert mock_wled.segment.call_count == 1
mock_wled.segment.assert_called_with(
cct=130,
on=True,
segment_id=0,
)