Home Assistant Unofficial Reference 2024.12.1
color.py
Go to the documentation of this file.
1 """Color util methods."""
2 
3 from __future__ import annotations
4 
5 import colorsys
6 import math
7 from typing import NamedTuple
8 
9 import attr
10 
11 from .scaling import scale_to_ranged_value
12 
13 
14 class RGBColor(NamedTuple):
15  """RGB hex values."""
16 
17  r: int
18  g: int
19  b: int
20 
21 
22 # Official CSS3 colors from w3.org:
23 # https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4
24 # names do not have spaces in them so that we can compare against
25 # requests more easily (by removing spaces from the requests as well).
26 # This lets "dark seagreen" and "dark sea green" both match the same
27 # color "darkseagreen".
28 COLORS = {
29  "aliceblue": RGBColor(240, 248, 255),
30  "antiquewhite": RGBColor(250, 235, 215),
31  "aqua": RGBColor(0, 255, 255),
32  "aquamarine": RGBColor(127, 255, 212),
33  "azure": RGBColor(240, 255, 255),
34  "beige": RGBColor(245, 245, 220),
35  "bisque": RGBColor(255, 228, 196),
36  "black": RGBColor(0, 0, 0),
37  "blanchedalmond": RGBColor(255, 235, 205),
38  "blue": RGBColor(0, 0, 255),
39  "blueviolet": RGBColor(138, 43, 226),
40  "brown": RGBColor(165, 42, 42),
41  "burlywood": RGBColor(222, 184, 135),
42  "cadetblue": RGBColor(95, 158, 160),
43  "chartreuse": RGBColor(127, 255, 0),
44  "chocolate": RGBColor(210, 105, 30),
45  "coral": RGBColor(255, 127, 80),
46  "cornflowerblue": RGBColor(100, 149, 237),
47  "cornsilk": RGBColor(255, 248, 220),
48  "crimson": RGBColor(220, 20, 60),
49  "cyan": RGBColor(0, 255, 255),
50  "darkblue": RGBColor(0, 0, 139),
51  "darkcyan": RGBColor(0, 139, 139),
52  "darkgoldenrod": RGBColor(184, 134, 11),
53  "darkgray": RGBColor(169, 169, 169),
54  "darkgreen": RGBColor(0, 100, 0),
55  "darkgrey": RGBColor(169, 169, 169),
56  "darkkhaki": RGBColor(189, 183, 107),
57  "darkmagenta": RGBColor(139, 0, 139),
58  "darkolivegreen": RGBColor(85, 107, 47),
59  "darkorange": RGBColor(255, 140, 0),
60  "darkorchid": RGBColor(153, 50, 204),
61  "darkred": RGBColor(139, 0, 0),
62  "darksalmon": RGBColor(233, 150, 122),
63  "darkseagreen": RGBColor(143, 188, 143),
64  "darkslateblue": RGBColor(72, 61, 139),
65  "darkslategray": RGBColor(47, 79, 79),
66  "darkslategrey": RGBColor(47, 79, 79),
67  "darkturquoise": RGBColor(0, 206, 209),
68  "darkviolet": RGBColor(148, 0, 211),
69  "deeppink": RGBColor(255, 20, 147),
70  "deepskyblue": RGBColor(0, 191, 255),
71  "dimgray": RGBColor(105, 105, 105),
72  "dimgrey": RGBColor(105, 105, 105),
73  "dodgerblue": RGBColor(30, 144, 255),
74  "firebrick": RGBColor(178, 34, 34),
75  "floralwhite": RGBColor(255, 250, 240),
76  "forestgreen": RGBColor(34, 139, 34),
77  "fuchsia": RGBColor(255, 0, 255),
78  "gainsboro": RGBColor(220, 220, 220),
79  "ghostwhite": RGBColor(248, 248, 255),
80  "gold": RGBColor(255, 215, 0),
81  "goldenrod": RGBColor(218, 165, 32),
82  "gray": RGBColor(128, 128, 128),
83  "green": RGBColor(0, 128, 0),
84  "greenyellow": RGBColor(173, 255, 47),
85  "grey": RGBColor(128, 128, 128),
86  "honeydew": RGBColor(240, 255, 240),
87  "hotpink": RGBColor(255, 105, 180),
88  "indianred": RGBColor(205, 92, 92),
89  "indigo": RGBColor(75, 0, 130),
90  "ivory": RGBColor(255, 255, 240),
91  "khaki": RGBColor(240, 230, 140),
92  "lavender": RGBColor(230, 230, 250),
93  "lavenderblush": RGBColor(255, 240, 245),
94  "lawngreen": RGBColor(124, 252, 0),
95  "lemonchiffon": RGBColor(255, 250, 205),
96  "lightblue": RGBColor(173, 216, 230),
97  "lightcoral": RGBColor(240, 128, 128),
98  "lightcyan": RGBColor(224, 255, 255),
99  "lightgoldenrodyellow": RGBColor(250, 250, 210),
100  "lightgray": RGBColor(211, 211, 211),
101  "lightgreen": RGBColor(144, 238, 144),
102  "lightgrey": RGBColor(211, 211, 211),
103  "lightpink": RGBColor(255, 182, 193),
104  "lightsalmon": RGBColor(255, 160, 122),
105  "lightseagreen": RGBColor(32, 178, 170),
106  "lightskyblue": RGBColor(135, 206, 250),
107  "lightslategray": RGBColor(119, 136, 153),
108  "lightslategrey": RGBColor(119, 136, 153),
109  "lightsteelblue": RGBColor(176, 196, 222),
110  "lightyellow": RGBColor(255, 255, 224),
111  "lime": RGBColor(0, 255, 0),
112  "limegreen": RGBColor(50, 205, 50),
113  "linen": RGBColor(250, 240, 230),
114  "magenta": RGBColor(255, 0, 255),
115  "maroon": RGBColor(128, 0, 0),
116  "mediumaquamarine": RGBColor(102, 205, 170),
117  "mediumblue": RGBColor(0, 0, 205),
118  "mediumorchid": RGBColor(186, 85, 211),
119  "mediumpurple": RGBColor(147, 112, 219),
120  "mediumseagreen": RGBColor(60, 179, 113),
121  "mediumslateblue": RGBColor(123, 104, 238),
122  "mediumspringgreen": RGBColor(0, 250, 154),
123  "mediumturquoise": RGBColor(72, 209, 204),
124  "mediumvioletred": RGBColor(199, 21, 133),
125  "midnightblue": RGBColor(25, 25, 112),
126  "mintcream": RGBColor(245, 255, 250),
127  "mistyrose": RGBColor(255, 228, 225),
128  "moccasin": RGBColor(255, 228, 181),
129  "navajowhite": RGBColor(255, 222, 173),
130  "navy": RGBColor(0, 0, 128),
131  "navyblue": RGBColor(0, 0, 128),
132  "oldlace": RGBColor(253, 245, 230),
133  "olive": RGBColor(128, 128, 0),
134  "olivedrab": RGBColor(107, 142, 35),
135  "orange": RGBColor(255, 165, 0),
136  "orangered": RGBColor(255, 69, 0),
137  "orchid": RGBColor(218, 112, 214),
138  "palegoldenrod": RGBColor(238, 232, 170),
139  "palegreen": RGBColor(152, 251, 152),
140  "paleturquoise": RGBColor(175, 238, 238),
141  "palevioletred": RGBColor(219, 112, 147),
142  "papayawhip": RGBColor(255, 239, 213),
143  "peachpuff": RGBColor(255, 218, 185),
144  "peru": RGBColor(205, 133, 63),
145  "pink": RGBColor(255, 192, 203),
146  "plum": RGBColor(221, 160, 221),
147  "powderblue": RGBColor(176, 224, 230),
148  "purple": RGBColor(128, 0, 128),
149  "red": RGBColor(255, 0, 0),
150  "rosybrown": RGBColor(188, 143, 143),
151  "royalblue": RGBColor(65, 105, 225),
152  "saddlebrown": RGBColor(139, 69, 19),
153  "salmon": RGBColor(250, 128, 114),
154  "sandybrown": RGBColor(244, 164, 96),
155  "seagreen": RGBColor(46, 139, 87),
156  "seashell": RGBColor(255, 245, 238),
157  "sienna": RGBColor(160, 82, 45),
158  "silver": RGBColor(192, 192, 192),
159  "skyblue": RGBColor(135, 206, 235),
160  "slateblue": RGBColor(106, 90, 205),
161  "slategray": RGBColor(112, 128, 144),
162  "slategrey": RGBColor(112, 128, 144),
163  "snow": RGBColor(255, 250, 250),
164  "springgreen": RGBColor(0, 255, 127),
165  "steelblue": RGBColor(70, 130, 180),
166  "tan": RGBColor(210, 180, 140),
167  "teal": RGBColor(0, 128, 128),
168  "thistle": RGBColor(216, 191, 216),
169  "tomato": RGBColor(255, 99, 71),
170  "turquoise": RGBColor(64, 224, 208),
171  "violet": RGBColor(238, 130, 238),
172  "wheat": RGBColor(245, 222, 179),
173  "white": RGBColor(255, 255, 255),
174  "whitesmoke": RGBColor(245, 245, 245),
175  "yellow": RGBColor(255, 255, 0),
176  "yellowgreen": RGBColor(154, 205, 50),
177  # And...
178  "homeassistant": RGBColor(24, 188, 242),
179 }
180 
181 
182 @attr.s()
183 class XYPoint:
184  """Represents a CIE 1931 XY coordinate pair."""
185 
186  x: float = attr.ib()
187  y: float = attr.ib()
188 
189 
190 @attr.s()
191 class GamutType:
192  """Represents the Gamut of a light."""
193 
194  # ColorGamut = gamut(xypoint(xR,yR),xypoint(xG,yG),xypoint(xB,yB))
195  red: XYPoint = attr.ib()
196  green: XYPoint = attr.ib()
197  blue: XYPoint = attr.ib()
198 
199 
200 def color_name_to_rgb(color_name: str) -> RGBColor:
201  """Convert color name to RGB hex value."""
202  # COLORS map has no spaces in it, so make the color_name have no
203  # spaces in it as well for matching purposes
204  hex_value = COLORS.get(color_name.replace(" ", "").lower())
205  if not hex_value:
206  raise ValueError("Unknown color")
207 
208  return hex_value
209 
210 
212  iR: int, iG: int, iB: int, Gamut: GamutType | None = None
213 ) -> tuple[float, float]:
214  """Convert from RGB color to XY color."""
215  return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2]
216 
217 
218 # Taken from:
219 # https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md
220 # License: Code is given as is. Use at your own risk and discretion.
222  iR: int, iG: int, iB: int, Gamut: GamutType | None = None
223 ) -> tuple[float, float, int]:
224  """Convert from RGB color to XY color."""
225  if iR + iG + iB == 0:
226  return 0.0, 0.0, 0
227 
228  R = iR / 255
229  B = iB / 255
230  G = iG / 255
231 
232  # Gamma correction
233  R = pow((R + 0.055) / (1.0 + 0.055), 2.4) if (R > 0.04045) else (R / 12.92)
234  G = pow((G + 0.055) / (1.0 + 0.055), 2.4) if (G > 0.04045) else (G / 12.92)
235  B = pow((B + 0.055) / (1.0 + 0.055), 2.4) if (B > 0.04045) else (B / 12.92)
236 
237  # Wide RGB D65 conversion formula
238  X = R * 0.664511 + G * 0.154324 + B * 0.162028
239  Y = R * 0.283881 + G * 0.668433 + B * 0.047685
240  Z = R * 0.000088 + G * 0.072310 + B * 0.986039
241 
242  # Convert XYZ to xy
243  x = X / (X + Y + Z)
244  y = Y / (X + Y + Z)
245 
246  # Brightness
247  Y = min(Y, 1)
248  brightness = round(Y * 255)
249 
250  # Check if the given xy value is within the color-reach of the lamp.
251  if Gamut:
252  in_reach = check_point_in_lamps_reach((x, y), Gamut)
253  if not in_reach:
254  xy_closest = get_closest_point_to_point((x, y), Gamut)
255  x = xy_closest[0]
256  y = xy_closest[1]
257 
258  return round(x, 3), round(y, 3), brightness
259 
260 
262  vX: float, vY: float, Gamut: GamutType | None = None
263 ) -> tuple[int, int, int]:
264  """Convert from XY to a normalized RGB."""
265  return color_xy_brightness_to_RGB(vX, vY, 255, Gamut)
266 
267 
268 # Converted to Python from Obj-C, original source from:
269 # https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md
271  vX: float, vY: float, ibrightness: int, Gamut: GamutType | None = None
272 ) -> tuple[int, int, int]:
273  """Convert from XYZ to RGB."""
274  if Gamut and not check_point_in_lamps_reach((vX, vY), Gamut):
275  xy_closest = get_closest_point_to_point((vX, vY), Gamut)
276  vX = xy_closest[0]
277  vY = xy_closest[1]
278 
279  brightness = ibrightness / 255.0
280  if brightness == 0.0:
281  return (0, 0, 0)
282 
283  Y = brightness
284 
285  if vY == 0.0:
286  vY += 0.00000000001
287 
288  X = (Y / vY) * vX
289  Z = (Y / vY) * (1 - vX - vY)
290 
291  # Convert to RGB using Wide RGB D65 conversion.
292  r = X * 1.656492 - Y * 0.354851 - Z * 0.255038
293  g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152
294  b = X * 0.051713 - Y * 0.121364 + Z * 1.011530
295 
296  # Apply reverse gamma correction.
297  r, g, b = (
298  12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055)
299  for x in (r, g, b)
300  )
301 
302  # Bring all negative components to zero.
303  r, g, b = (max(0, x) for x in (r, g, b))
304 
305  # If one component is greater than 1, weight components by that value.
306  max_component = max(r, g, b)
307  if max_component > 1:
308  r, g, b = (x / max_component for x in (r, g, b))
309 
310  ir, ig, ib = (int(x * 255) for x in (r, g, b))
311 
312  return (ir, ig, ib)
313 
314 
315 def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> tuple[int, int, int]:
316  """Convert a hsb into its rgb representation."""
317  if fS == 0.0:
318  fV = int(fB * 255)
319  return fV, fV, fV
320 
321  r = g = b = 0
322  h = fH / 60
323  f = h - float(math.floor(h))
324  p = fB * (1 - fS)
325  q = fB * (1 - fS * f)
326  t = fB * (1 - (fS * (1 - f)))
327 
328  if int(h) == 0:
329  r = int(fB * 255)
330  g = int(t * 255)
331  b = int(p * 255)
332  elif int(h) == 1:
333  r = int(q * 255)
334  g = int(fB * 255)
335  b = int(p * 255)
336  elif int(h) == 2:
337  r = int(p * 255)
338  g = int(fB * 255)
339  b = int(t * 255)
340  elif int(h) == 3:
341  r = int(p * 255)
342  g = int(q * 255)
343  b = int(fB * 255)
344  elif int(h) == 4:
345  r = int(t * 255)
346  g = int(p * 255)
347  b = int(fB * 255)
348  elif int(h) == 5:
349  r = int(fB * 255)
350  g = int(p * 255)
351  b = int(q * 255)
352 
353  return (r, g, b)
354 
355 
356 def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> tuple[float, float, float]:
357  """Convert an rgb color to its hsv representation.
358 
359  Hue is scaled 0-360
360  Sat is scaled 0-100
361  Val is scaled 0-100
362  """
363  fHSV = colorsys.rgb_to_hsv(iR / 255.0, iG / 255.0, iB / 255.0)
364  return round(fHSV[0] * 360, 3), round(fHSV[1] * 100, 3), round(fHSV[2] * 100, 3)
365 
366 
367 def color_RGB_to_hs(iR: float, iG: float, iB: float) -> tuple[float, float]:
368  """Convert an rgb color to its hs representation."""
369  return color_RGB_to_hsv(iR, iG, iB)[:2]
370 
371 
372 def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> tuple[int, int, int]:
373  """Convert an hsv color into its rgb representation.
374 
375  Hue is scaled 0-360
376  Sat is scaled 0-100
377  Val is scaled 0-100
378  """
379  fRGB = colorsys.hsv_to_rgb(iH / 360, iS / 100, iV / 100)
380  return (round(fRGB[0] * 255), round(fRGB[1] * 255), round(fRGB[2] * 255))
381 
382 
383 def color_hs_to_RGB(iH: float, iS: float) -> tuple[int, int, int]:
384  """Convert an hsv color into its rgb representation."""
385  return color_hsv_to_RGB(iH, iS, 100)
386 
387 
389  vX: float, vY: float, Gamut: GamutType | None = None
390 ) -> tuple[float, float]:
391  """Convert an xy color to its hs representation."""
392  h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut))
393  return h, s
394 
395 
397  iH: float, iS: float, Gamut: GamutType | None = None
398 ) -> tuple[float, float]:
399  """Convert an hs color to its xy representation."""
400  return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut)
401 
402 
404  input_colors: tuple[int, ...], output_colors: tuple[float, ...]
405 ) -> tuple[int, ...]:
406  """Match the maximum value of the output to the input."""
407  max_in = max(input_colors)
408  max_out = max(output_colors)
409  if max_out == 0:
410  factor = 0.0
411  else:
412  factor = max_in / max_out
413  return tuple(int(round(i * factor)) for i in output_colors)
414 
415 
416 def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]:
417  """Convert an rgb color to an rgbw representation."""
418  # Calculate the white channel as the minimum of input rgb channels.
419  # Subtract the white portion from the remaining rgb channels.
420  w = min(r, g, b)
421  rgbw = (r - w, g - w, b - w, w)
422 
423  # Match the output maximum value to the input. This ensures the full
424  # channel range is used.
425  return match_max_scale((r, g, b), rgbw) # type: ignore[return-value]
426 
427 
428 def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]:
429  """Convert an rgbw color to an rgb representation."""
430  # Add the white channel to the rgb channels.
431  rgb = (r + w, g + w, b + w)
432 
433  # Match the output maximum value to the input. This ensures the
434  # output doesn't overflow.
435  return match_max_scale((r, g, b, w), rgb) # type: ignore[return-value]
436 
437 
439  r: int, g: int, b: int, min_kelvin: int, max_kelvin: int
440 ) -> tuple[int, int, int, int, int]:
441  """Convert an rgb color to an rgbww representation."""
442  # Find the color temperature when both white channels have equal brightness
443  max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
444  min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
445  mired_range = max_mireds - min_mireds
446  mired_midpoint = min_mireds + mired_range / 2
447  color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint)
448  w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
449 
450  # Find the ratio of the midpoint white in the input rgb channels
451  white_level = min(
452  r / w_r if w_r else 0, g / w_g if w_g else 0, b / w_b if w_b else 0
453  )
454 
455  # Subtract the white portion from the rgb channels.
456  rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level)
457  rgbww = (*rgb, round(white_level * 255), round(white_level * 255))
458 
459  # Match the output maximum value to the input. This ensures the full
460  # channel range is used.
461  return match_max_scale((r, g, b), rgbww) # type: ignore[return-value]
462 
463 
465  r: int, g: int, b: int, cw: int, ww: int, min_kelvin: int, max_kelvin: int
466 ) -> tuple[int, int, int]:
467  """Convert an rgbww color to an rgb representation."""
468  # Calculate color temperature of the white channels
469  max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
470  min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
471  mired_range = max_mireds - min_mireds
472  try:
473  ct_ratio = ww / (cw + ww)
474  except ZeroDivisionError:
475  ct_ratio = 0.5
476  color_temp_mired = min_mireds + ct_ratio * mired_range
477  if color_temp_mired:
478  color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
479  else:
480  color_temp_kelvin = 0
481  w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
482  white_level = max(cw, ww) / 255
483 
484  # Add the white channels to the rgb channels.
485  rgb = (r + w_r * white_level, g + w_g * white_level, b + w_b * white_level)
486 
487  # Match the output maximum value to the input. This ensures the
488  # output doesn't overflow.
489  return match_max_scale((r, g, b, cw, ww), rgb) # type: ignore[return-value]
490 
491 
492 def color_rgb_to_hex(r: int, g: int, b: int) -> str:
493  """Return a RGB color from a hex color string."""
494  return f"{round(r):02x}{round(g):02x}{round(b):02x}"
495 
496 
497 def rgb_hex_to_rgb_list(hex_string: str) -> list[int]:
498  """Return an RGB color value list from a hex color string."""
499  return [
500  int(hex_string[i : i + len(hex_string) // 3], 16)
501  for i in range(0, len(hex_string), len(hex_string) // 3)
502  ]
503 
504 
505 def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, float]:
506  """Return an hs color from a color temperature in Kelvin."""
507  return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin))
508 
509 
511  color_temperature_kelvin: float,
512 ) -> tuple[float, float, float]:
513  """Return an RGB color from a color temperature in Kelvin.
514 
515  This is a rough approximation based on the formula provided by T. Helland
516  http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
517  """
518  # range check
519  if color_temperature_kelvin < 1000:
520  color_temperature_kelvin = 1000
521  elif color_temperature_kelvin > 40000:
522  color_temperature_kelvin = 40000
523 
524  tmp_internal = color_temperature_kelvin / 100.0
525 
526  red = _get_red(tmp_internal)
527 
528  green = _get_green(tmp_internal)
529 
530  blue = _get_blue(tmp_internal)
531 
532  return red, green, blue
533 
534 
536  temperature: int, brightness: int, min_kelvin: int, max_kelvin: int
537 ) -> tuple[int, int, int, int, int]:
538  """Convert color temperature in kelvin to rgbcw.
539 
540  Returns a (r, g, b, cw, ww) tuple.
541  """
542  max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
543  min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
544  temperature = color_temperature_kelvin_to_mired(temperature)
545  mired_range = max_mireds - min_mireds
546  cold = ((max_mireds - temperature) / mired_range) * brightness
547  warm = brightness - cold
548  return (0, 0, 0, round(cold), round(warm))
549 
550 
552  rgbww: tuple[int, int, int, int, int], min_kelvin: int, max_kelvin: int
553 ) -> tuple[int, int]:
554  """Convert rgbcw to color temperature in kelvin.
555 
556  Returns a tuple (color_temperature, brightness).
557  """
558  _, _, _, cold, warm = rgbww
559  return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin)
560 
561 
563  cold: int, warm: int, min_kelvin: int, max_kelvin: int
564 ) -> tuple[int, int]:
565  """Convert whites to color temperature in kelvin.
566 
567  Returns a tuple (color_temperature, brightness).
568  """
569  max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
570  min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
571  brightness = warm / 255 + cold / 255
572  if brightness == 0:
573  # Return the warmest color if brightness is 0
574  return (min_kelvin, 0)
575  return round(
577  ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds
578  )
579  ), min(255, round(brightness * 255))
580 
581 
582 def color_xy_to_temperature(x: float, y: float) -> int:
583  """Convert an xy color to a color temperature in Kelvin.
584 
585  Uses McCamy's approximation (https://doi.org/10.1002/col.5080170211),
586  close enough for uses between 2000 K and 10000 K.
587  """
588  n = (x - 0.3320) / (0.1858 - y)
589  CCT = 437 * (n**3) + 3601 * (n**2) + 6861 * n + 5517
590 
591  return int(CCT)
592 
593 
594 def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float:
595  """Clamp the given color component value between the given min and max values.
596 
597  The range defined by the minimum and maximum values is inclusive, i.e. given a
598  color_component of 0 and a minimum of 10, the returned value is 10.
599  """
600  color_component_out = max(color_component, minimum)
601  return min(color_component_out, maximum)
602 
603 
604 def _get_red(temperature: float) -> float:
605  """Get the red component of the temperature in RGB space."""
606  if temperature <= 66:
607  return 255
608  tmp_red = 329.698727446 * math.pow(temperature - 60, -0.1332047592)
609  return _clamp(tmp_red)
610 
611 
612 def _get_green(temperature: float) -> float:
613  """Get the green component of the given color temp in RGB space."""
614  if temperature <= 66:
615  green = 99.4708025861 * math.log(temperature) - 161.1195681661
616  else:
617  green = 288.1221695283 * math.pow(temperature - 60, -0.0755148492)
618  return _clamp(green)
619 
620 
621 def _get_blue(temperature: float) -> float:
622  """Get the blue component of the given color temperature in RGB space."""
623  if temperature >= 66:
624  return 255
625  if temperature <= 19:
626  return 0
627  blue = 138.5177312231 * math.log(temperature - 10) - 305.0447927307
628  return _clamp(blue)
629 
630 
631 def color_temperature_mired_to_kelvin(mired_temperature: float) -> int:
632  """Convert absolute mired shift to degrees kelvin."""
633  return math.floor(1000000 / mired_temperature)
634 
635 
636 def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> int:
637  """Convert degrees kelvin to mired shift."""
638  return math.floor(1000000 / kelvin_temperature)
639 
640 
641 # The following 5 functions are adapted from rgbxy provided by Benjamin Knight
642 # License: The MIT License (MIT), 2014.
643 # https://github.com/benknight/hue-python-rgb-converter
644 def cross_product(p1: XYPoint, p2: XYPoint) -> float:
645  """Calculate the cross product of two XYPoints."""
646  return float(p1.x * p2.y - p1.y * p2.x)
647 
648 
649 def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float:
650  """Calculate the distance between two XYPoints."""
651  dx = one.x - two.x
652  dy = one.y - two.y
653  return math.sqrt(dx * dx + dy * dy)
654 
655 
656 def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint:
657  """Find the closest point from P to a line defined by A and B.
658 
659  This point will be reproducible by the lamp
660  as it is on the edge of the gamut.
661  """
662  AP = XYPoint(P.x - A.x, P.y - A.y)
663  AB = XYPoint(B.x - A.x, B.y - A.y)
664  ab2 = AB.x * AB.x + AB.y * AB.y
665  ap_ab = AP.x * AB.x + AP.y * AB.y
666  t = ap_ab / ab2
667 
668  if t < 0.0:
669  t = 0.0
670  elif t > 1.0:
671  t = 1.0
672 
673  return XYPoint(A.x + AB.x * t, A.y + AB.y * t)
674 
675 
677  xy_tuple: tuple[float, float], Gamut: GamutType
678 ) -> tuple[float, float]:
679  """Get the closest matching color within the gamut of the light.
680 
681  Should only be used if the supplied color is outside of the color gamut.
682  """
683  xy_point = XYPoint(xy_tuple[0], xy_tuple[1])
684 
685  # find the closest point on each line in the CIE 1931 'triangle'.
686  pAB = get_closest_point_to_line(Gamut.red, Gamut.green, xy_point)
687  pAC = get_closest_point_to_line(Gamut.blue, Gamut.red, xy_point)
688  pBC = get_closest_point_to_line(Gamut.green, Gamut.blue, xy_point)
689 
690  # Get the distances per point and see which point is closer to our Point.
691  dAB = get_distance_between_two_points(xy_point, pAB)
692  dAC = get_distance_between_two_points(xy_point, pAC)
693  dBC = get_distance_between_two_points(xy_point, pBC)
694 
695  lowest = dAB
696  closest_point = pAB
697 
698  if dAC < lowest:
699  lowest = dAC
700  closest_point = pAC
701 
702  if dBC < lowest:
703  lowest = dBC
704  closest_point = pBC
705 
706  # Change the xy value to a value which is within the reach of the lamp.
707  cx = closest_point.x
708  cy = closest_point.y
709 
710  return (cx, cy)
711 
712 
713 def check_point_in_lamps_reach(p: tuple[float, float], Gamut: GamutType) -> bool:
714  """Check if the provided XYPoint can be recreated by a Hue lamp."""
715  v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y)
716  v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y)
717 
718  q = XYPoint(p[0] - Gamut.red.x, p[1] - Gamut.red.y)
719  s = cross_product(q, v2) / cross_product(v1, v2)
720  t = cross_product(v1, q) / cross_product(v1, v2)
721 
722  return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0)
723 
724 
725 def check_valid_gamut(Gamut: GamutType) -> bool:
726  """Check if the supplied gamut is valid."""
727  # Check if the three points of the supplied gamut are not on the same line.
728  v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y)
729  v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y)
730  not_on_line = cross_product(v1, v2) > 0.0001
731 
732  # Check if all six coordinates of the gamut lie between 0 and 1.
733  red_valid = (
734  Gamut.red.x >= 0 and Gamut.red.x <= 1 and Gamut.red.y >= 0 and Gamut.red.y <= 1
735  )
736  green_valid = (
737  Gamut.green.x >= 0
738  and Gamut.green.x <= 1
739  and Gamut.green.y >= 0
740  and Gamut.green.y <= 1
741  )
742  blue_valid = (
743  Gamut.blue.x >= 0
744  and Gamut.blue.x <= 1
745  and Gamut.blue.y >= 0
746  and Gamut.blue.y <= 1
747  )
748 
749  return not_on_line and red_valid and green_valid and blue_valid
750 
751 
752 def brightness_to_value(low_high_range: tuple[float, float], brightness: int) -> float:
753  """Given a brightness_scale convert a brightness to a single value.
754 
755  Do not include 0 if the light is off for value 0.
756 
757  Given a brightness low_high_range of (1,100) this function
758  will return:
759 
760  255: 100.0
761  127: ~49.8039
762  10: ~3.9216
763  """
764  return scale_to_ranged_value((1, 255), low_high_range, brightness)
765 
766 
767 def value_to_brightness(low_high_range: tuple[float, float], value: float) -> int:
768  """Given a brightness_scale convert a single value to a brightness.
769 
770  Do not include 0 if the light is off for value 0.
771 
772  Given a brightness low_high_range of (1,100) this function
773  will return:
774 
775  100: 255
776  50: 128
777  4: 10
778 
779  The value will be clamped between 1..255 to ensure valid value.
780  """
781  return min(
782  255,
783  max(1, round(scale_to_ranged_value(low_high_range, (1, 255), value))),
784  )
int color_temperature_mired_to_kelvin(float mired_temperature)
Definition: color.py:631
list[int] rgb_hex_to_rgb_list(str hex_string)
Definition: color.py:497
float _get_blue(float temperature)
Definition: color.py:621
float _clamp(float color_component, float minimum=0, float maximum=255)
Definition: color.py:594
float _get_red(float temperature)
Definition: color.py:604
tuple[int, int, int] color_hs_to_RGB(float iH, float iS)
Definition: color.py:383
tuple[int, int] _white_levels_to_color_temperature(int cold, int warm, int min_kelvin, int max_kelvin)
Definition: color.py:564
tuple[float, float] get_closest_point_to_point(tuple[float, float] xy_tuple, GamutType Gamut)
Definition: color.py:678
tuple[int, int, int] color_xy_to_RGB(float vX, float vY, GamutType|None Gamut=None)
Definition: color.py:263
float get_distance_between_two_points(XYPoint one, XYPoint two)
Definition: color.py:649
tuple[int, int, int] color_rgbw_to_rgb(int r, int g, int b, int w)
Definition: color.py:428
XYPoint get_closest_point_to_line(XYPoint A, XYPoint B, XYPoint P)
Definition: color.py:656
float brightness_to_value(tuple[float, float] low_high_range, int brightness)
Definition: color.py:752
tuple[int, int, int, int] color_rgb_to_rgbw(int r, int g, int b)
Definition: color.py:416
tuple[int, int, int] color_hsv_to_RGB(float iH, float iS, float iV)
Definition: color.py:372
tuple[float, float] color_RGB_to_hs(float iR, float iG, float iB)
Definition: color.py:367
tuple[int, int, int] color_xy_brightness_to_RGB(float vX, float vY, int ibrightness, GamutType|None Gamut=None)
Definition: color.py:272
RGBColor color_name_to_rgb(str color_name)
Definition: color.py:200
tuple[float, float] color_RGB_to_xy(int iR, int iG, int iB, GamutType|None Gamut=None)
Definition: color.py:213
float _get_green(float temperature)
Definition: color.py:612
bool check_point_in_lamps_reach(tuple[float, float] p, GamutType Gamut)
Definition: color.py:713
tuple[float, float] color_xy_to_hs(float vX, float vY, GamutType|None Gamut=None)
Definition: color.py:390
tuple[float, float] color_temperature_to_hs(float color_temperature_kelvin)
Definition: color.py:505
tuple[int, int, int] color_rgbww_to_rgb(int r, int g, int b, int cw, int ww, int min_kelvin, int max_kelvin)
Definition: color.py:466
tuple[float, float, int] color_RGB_to_xy_brightness(int iR, int iG, int iB, GamutType|None Gamut=None)
Definition: color.py:223
int value_to_brightness(tuple[float, float] low_high_range, float value)
Definition: color.py:767
tuple[int, int] rgbww_to_color_temperature(tuple[int, int, int, int, int] rgbww, int min_kelvin, int max_kelvin)
Definition: color.py:553
tuple[float, float] color_hs_to_xy(float iH, float iS, GamutType|None Gamut=None)
Definition: color.py:398
tuple[int, int, int, int, int] color_temperature_to_rgbww(int temperature, int brightness, int min_kelvin, int max_kelvin)
Definition: color.py:537
tuple[int, int, int] color_hsb_to_RGB(float fH, float fS, float fB)
Definition: color.py:315
float cross_product(XYPoint p1, XYPoint p2)
Definition: color.py:644
tuple[float, float, float] color_RGB_to_hsv(float iR, float iG, float iB)
Definition: color.py:356
int color_xy_to_temperature(float x, float y)
Definition: color.py:582
str color_rgb_to_hex(int r, int g, int b)
Definition: color.py:492
bool check_valid_gamut(GamutType Gamut)
Definition: color.py:725
tuple[int,...] match_max_scale(tuple[int,...] input_colors, tuple[float,...] output_colors)
Definition: color.py:405
tuple[float, float, float] color_temperature_to_rgb(float color_temperature_kelvin)
Definition: color.py:512
int color_temperature_kelvin_to_mired(float kelvin_temperature)
Definition: color.py:636
tuple[int, int, int, int, int] color_rgb_to_rgbww(int r, int g, int b, int min_kelvin, int max_kelvin)
Definition: color.py:440
float scale_to_ranged_value(tuple[float, float] source_low_high_range, tuple[float, float] target_low_high_range, float value)
Definition: scaling.py:35