Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Provides functionality to interact with lights."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 import csv
7 import dataclasses
8 from datetime import timedelta
9 from enum import IntFlag, StrEnum
10 import logging
11 import os
12 from typing import Any, Self, cast, final
13 
14 from propcache import cached_property
15 import voluptuous as vol
16 
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import (
19  SERVICE_TOGGLE,
20  SERVICE_TURN_OFF,
21  SERVICE_TURN_ON,
22  STATE_ON,
23 )
24 from homeassistant.core import HomeAssistant, ServiceCall, callback
25 from homeassistant.exceptions import HomeAssistantError
26 from homeassistant.helpers import config_validation as cv, entity_registry as er
27 from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
28 from homeassistant.helpers.entity_component import EntityComponent
29 from homeassistant.helpers.typing import ConfigType, VolDictType
30 from homeassistant.loader import bind_hass
31 import homeassistant.util.color as color_util
32 from homeassistant.util.hass_dict import HassKey
33 
34 DOMAIN = "light"
35 DATA_COMPONENT: HassKey[EntityComponent[LightEntity]] = HassKey(DOMAIN)
36 ENTITY_ID_FORMAT = DOMAIN + ".{}"
37 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
38 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
39 SCAN_INTERVAL = timedelta(seconds=30)
40 
41 DATA_PROFILES: HassKey[Profiles] = HassKey(f"{DOMAIN}_profiles")
42 
43 
44 class LightEntityFeature(IntFlag):
45  """Supported features of the light entity."""
46 
47  EFFECT = 4
48  FLASH = 8
49  TRANSITION = 32
50 
51 
52 # These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
53 # Please use the LightEntityFeature enum instead.
54 SUPPORT_BRIGHTNESS = 1 # Deprecated, replaced by color modes
55 SUPPORT_COLOR_TEMP = 2 # Deprecated, replaced by color modes
56 SUPPORT_EFFECT = 4
57 SUPPORT_FLASH = 8
58 SUPPORT_COLOR = 16 # Deprecated, replaced by color modes
59 SUPPORT_TRANSITION = 32
60 
61 # Color mode of the light
62 ATTR_COLOR_MODE = "color_mode"
63 # List of color modes supported by the light
64 ATTR_SUPPORTED_COLOR_MODES = "supported_color_modes"
65 
66 
67 class ColorMode(StrEnum):
68  """Possible light color modes."""
69 
70  UNKNOWN = "unknown"
71  """Ambiguous color mode"""
72  ONOFF = "onoff"
73  """Must be the only supported mode"""
74  BRIGHTNESS = "brightness"
75  """Must be the only supported mode"""
76  COLOR_TEMP = "color_temp"
77  HS = "hs"
78  XY = "xy"
79  RGB = "rgb"
80  RGBW = "rgbw"
81  RGBWW = "rgbww"
82  WHITE = "white"
83  """Must *NOT* be the only supported mode"""
84 
85 
86 # These COLOR_MODE_* constants are deprecated as of Home Assistant 2022.5.
87 # Please use the LightEntityFeature enum instead.
88 COLOR_MODE_UNKNOWN = "unknown"
89 COLOR_MODE_ONOFF = "onoff"
90 COLOR_MODE_BRIGHTNESS = "brightness"
91 COLOR_MODE_COLOR_TEMP = "color_temp"
92 COLOR_MODE_HS = "hs"
93 COLOR_MODE_XY = "xy"
94 COLOR_MODE_RGB = "rgb"
95 COLOR_MODE_RGBW = "rgbw"
96 COLOR_MODE_RGBWW = "rgbww"
97 COLOR_MODE_WHITE = "white"
98 
99 VALID_COLOR_MODES = {
100  ColorMode.ONOFF,
101  ColorMode.BRIGHTNESS,
102  ColorMode.COLOR_TEMP,
103  ColorMode.HS,
104  ColorMode.XY,
105  ColorMode.RGB,
106  ColorMode.RGBW,
107  ColorMode.RGBWW,
108  ColorMode.WHITE,
109 }
110 COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {ColorMode.ONOFF}
111 COLOR_MODES_COLOR = {
112  ColorMode.HS,
113  ColorMode.RGB,
114  ColorMode.RGBW,
115  ColorMode.RGBWW,
116  ColorMode.XY,
117 }
118 
119 # mypy: disallow-any-generics
120 
121 
122 def filter_supported_color_modes(color_modes: Iterable[ColorMode]) -> set[ColorMode]:
123  """Filter the given color modes."""
124  color_modes = set(color_modes)
125  if (
126  not color_modes
127  or ColorMode.UNKNOWN in color_modes
128  or (ColorMode.WHITE in color_modes and not color_supported(color_modes))
129  ):
130  raise HomeAssistantError
131 
132  if ColorMode.ONOFF in color_modes and len(color_modes) > 1:
133  color_modes.remove(ColorMode.ONOFF)
134  if ColorMode.BRIGHTNESS in color_modes and len(color_modes) > 1:
135  color_modes.remove(ColorMode.BRIGHTNESS)
136  return color_modes
137 
138 
140  color_modes: Iterable[ColorMode | str],
141 ) -> set[ColorMode | str]:
142  """Validate the given color modes."""
143  color_modes = set(color_modes)
144  if (
145  not color_modes
146  or ColorMode.UNKNOWN in color_modes
147  or (ColorMode.BRIGHTNESS in color_modes and len(color_modes) > 1)
148  or (ColorMode.ONOFF in color_modes and len(color_modes) > 1)
149  or (ColorMode.WHITE in color_modes and not color_supported(color_modes))
150  ):
151  raise vol.Error(f"Invalid supported_color_modes {sorted(color_modes)}")
152  return color_modes
153 
154 
155 def brightness_supported(color_modes: Iterable[ColorMode | str] | None) -> bool:
156  """Test if brightness is supported."""
157  if not color_modes:
158  return False
159  return not COLOR_MODES_BRIGHTNESS.isdisjoint(color_modes)
160 
161 
162 def color_supported(color_modes: Iterable[ColorMode | str] | None) -> bool:
163  """Test if color is supported."""
164  if not color_modes:
165  return False
166  return not COLOR_MODES_COLOR.isdisjoint(color_modes)
167 
168 
169 def color_temp_supported(color_modes: Iterable[ColorMode | str] | None) -> bool:
170  """Test if color temperature is supported."""
171  if not color_modes:
172  return False
173  return ColorMode.COLOR_TEMP in color_modes
174 
175 
176 def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | None:
177  """Get supported color modes for a light entity.
178 
179  First try the statemachine, then entity registry.
180  This is the equivalent of entity helper get_supported_features.
181  """
182  if state := hass.states.get(entity_id):
183  return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES)
184 
185  entity_registry = er.async_get(hass)
186  if not (entry := entity_registry.async_get(entity_id)):
187  raise HomeAssistantError(f"Unknown entity {entity_id}")
188  if not entry.capabilities:
189  return None
190 
191  return entry.capabilities.get(ATTR_SUPPORTED_COLOR_MODES)
192 
193 
194 # Float that represents transition time in seconds to make change.
195 ATTR_TRANSITION = "transition"
196 
197 # Lists holding color values
198 ATTR_RGB_COLOR = "rgb_color"
199 ATTR_RGBW_COLOR = "rgbw_color"
200 ATTR_RGBWW_COLOR = "rgbww_color"
201 ATTR_XY_COLOR = "xy_color"
202 ATTR_HS_COLOR = "hs_color"
203 ATTR_COLOR_TEMP = "color_temp" # Deprecated in HA Core 2022.11
204 ATTR_KELVIN = "kelvin" # Deprecated in HA Core 2022.11
205 ATTR_MIN_MIREDS = "min_mireds" # Deprecated in HA Core 2022.11
206 ATTR_MAX_MIREDS = "max_mireds" # Deprecated in HA Core 2022.11
207 ATTR_COLOR_TEMP_KELVIN = "color_temp_kelvin"
208 ATTR_MIN_COLOR_TEMP_KELVIN = "min_color_temp_kelvin"
209 ATTR_MAX_COLOR_TEMP_KELVIN = "max_color_temp_kelvin"
210 ATTR_COLOR_NAME = "color_name"
211 ATTR_WHITE = "white"
212 
213 # Brightness of the light, 0..255 or percentage
214 ATTR_BRIGHTNESS = "brightness"
215 ATTR_BRIGHTNESS_PCT = "brightness_pct"
216 ATTR_BRIGHTNESS_STEP = "brightness_step"
217 ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct"
218 
219 # String representing a profile (built-in ones or external defined).
220 ATTR_PROFILE = "profile"
221 
222 # If the light should flash, can be FLASH_SHORT or FLASH_LONG.
223 ATTR_FLASH = "flash"
224 FLASH_SHORT = "short"
225 FLASH_LONG = "long"
226 
227 # List of possible effects
228 ATTR_EFFECT_LIST = "effect_list"
229 
230 # Apply an effect to the light, can be EFFECT_COLORLOOP.
231 ATTR_EFFECT = "effect"
232 EFFECT_COLORLOOP = "colorloop"
233 EFFECT_OFF = "off"
234 EFFECT_RANDOM = "random"
235 EFFECT_WHITE = "white"
236 
237 COLOR_GROUP = "Color descriptors"
238 
239 LIGHT_PROFILES_FILE = "light_profiles.csv"
240 
241 # Service call validation schemas
242 VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553))
243 VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
244 VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100))
245 VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255))
246 VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100))
247 VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG])
248 
249 LIGHT_TURN_ON_SCHEMA: VolDictType = {
250  vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string,
251  ATTR_TRANSITION: VALID_TRANSITION,
252  vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS,
253  vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT,
254  vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP,
255  vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT,
256  vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string,
257  vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(
258  vol.Coerce(int), vol.Range(min=1)
259  ),
260  vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): cv.positive_int,
261  vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int,
262  vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All(
263  vol.Coerce(tuple),
264  vol.ExactSequence(
265  (
266  vol.All(vol.Coerce(float), vol.Range(min=0, max=360)),
267  vol.All(vol.Coerce(float), vol.Range(min=0, max=100)),
268  )
269  ),
270  ),
271  vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All(
272  vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3)
273  ),
274  vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All(
275  vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 4)
276  ),
277  vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All(
278  vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 5)
279  ),
280  vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(
281  vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float))
282  ),
283  vol.Exclusive(ATTR_WHITE, COLOR_GROUP): vol.Any(True, VALID_BRIGHTNESS),
284  ATTR_FLASH: VALID_FLASH,
285  ATTR_EFFECT: cv.string,
286 }
287 
288 LIGHT_TURN_OFF_SCHEMA: VolDictType = {
289  ATTR_TRANSITION: VALID_TRANSITION,
290  ATTR_FLASH: VALID_FLASH,
291 }
292 
293 
294 _LOGGER = logging.getLogger(__name__)
295 
296 
297 @bind_hass
298 def is_on(hass: HomeAssistant, entity_id: str) -> bool:
299  """Return if the lights are on based on the statemachine."""
300  return hass.states.is_state(entity_id, STATE_ON)
301 
302 
304  hass: HomeAssistant, params: dict[str, Any]
305 ) -> None:
306  """Process extra data for turn light on request.
307 
308  Async friendly.
309  """
310  # Bail out, we process this later.
311  if ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params:
312  return
313 
314  if ATTR_PROFILE in params:
315  hass.data[DATA_PROFILES].apply_profile(params.pop(ATTR_PROFILE), params)
316 
317  if (color_name := params.pop(ATTR_COLOR_NAME, None)) is not None:
318  try:
319  params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
320  except ValueError:
321  _LOGGER.warning("Got unknown color %s, falling back to white", color_name)
322  params[ATTR_RGB_COLOR] = (255, 255, 255)
323 
324  if (mired := params.pop(ATTR_COLOR_TEMP, None)) is not None:
325  kelvin = color_util.color_temperature_mired_to_kelvin(mired)
326  params[ATTR_COLOR_TEMP] = int(mired)
327  params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)
328 
329  if (kelvin := params.pop(ATTR_KELVIN, None)) is not None:
330  mired = color_util.color_temperature_kelvin_to_mired(kelvin)
331  params[ATTR_COLOR_TEMP] = int(mired)
332  params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)
333 
334  if (kelvin := params.pop(ATTR_COLOR_TEMP_KELVIN, None)) is not None:
335  mired = color_util.color_temperature_kelvin_to_mired(kelvin)
336  params[ATTR_COLOR_TEMP] = int(mired)
337  params[ATTR_COLOR_TEMP_KELVIN] = int(kelvin)
338 
339  brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None)
340  if brightness_pct is not None:
341  params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100)
342 
343 
345  light: LightEntity, params: dict[str, Any]
346 ) -> dict[str, Any]:
347  """Filter out params not used in turn off or not supported by the light."""
348  if not params:
349  return params
350 
351  supported_features = light.supported_features_compat
352 
353  if LightEntityFeature.FLASH not in supported_features:
354  params.pop(ATTR_FLASH, None)
355  if LightEntityFeature.TRANSITION not in supported_features:
356  params.pop(ATTR_TRANSITION, None)
357 
358  return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)}
359 
360 
361 def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]:
362  """Filter out params not supported by the light."""
363  supported_features = light.supported_features_compat
364 
365  if LightEntityFeature.EFFECT not in supported_features:
366  params.pop(ATTR_EFFECT, None)
367  if LightEntityFeature.FLASH not in supported_features:
368  params.pop(ATTR_FLASH, None)
369  if LightEntityFeature.TRANSITION not in supported_features:
370  params.pop(ATTR_TRANSITION, None)
371 
372  supported_color_modes = (
373  light._light_internal_supported_color_modes # noqa: SLF001
374  )
375  if not brightness_supported(supported_color_modes):
376  params.pop(ATTR_BRIGHTNESS, None)
377  if ColorMode.COLOR_TEMP not in supported_color_modes:
378  params.pop(ATTR_COLOR_TEMP, None)
379  params.pop(ATTR_COLOR_TEMP_KELVIN, None)
380  if ColorMode.HS not in supported_color_modes:
381  params.pop(ATTR_HS_COLOR, None)
382  if ColorMode.RGB not in supported_color_modes:
383  params.pop(ATTR_RGB_COLOR, None)
384  if ColorMode.RGBW not in supported_color_modes:
385  params.pop(ATTR_RGBW_COLOR, None)
386  if ColorMode.RGBWW not in supported_color_modes:
387  params.pop(ATTR_RGBWW_COLOR, None)
388  if ColorMode.WHITE not in supported_color_modes:
389  params.pop(ATTR_WHITE, None)
390  if ColorMode.XY not in supported_color_modes:
391  params.pop(ATTR_XY_COLOR, None)
392 
393  return params
394 
395 
396 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
397  """Expose light control via state machine and services."""
398  component = hass.data[DATA_COMPONENT] = EntityComponent[LightEntity](
399  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
400  )
401  await component.async_setup(config)
402 
403  profiles = hass.data[DATA_PROFILES] = Profiles(hass)
404  # Profiles are loaded in a separate task to avoid delaying the setup
405  # of the light base platform.
406  hass.async_create_task(profiles.async_initialize(), eager_start=True)
407 
408  def preprocess_data(data: dict[str, Any]) -> VolDictType:
409  """Preprocess the service data."""
410  base: VolDictType = {
411  entity_field: data.pop(entity_field) # type: ignore[arg-type]
412  for entity_field in cv.ENTITY_SERVICE_FIELDS
413  if entity_field in data
414  }
415 
417  base["params"] = data
418  return base
419 
420  async def async_handle_light_on_service( # noqa: C901
421  light: LightEntity, call: ServiceCall
422  ) -> None:
423  """Handle turning a light on.
424 
425  If brightness is set to 0, this service will turn the light off.
426  """
427  params: dict[str, Any] = dict(call.data["params"])
428 
429  # Only process params once we processed brightness step
430  if params and (
431  ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params
432  ):
433  brightness = light.brightness if light.is_on and light.brightness else 0
434 
435  if ATTR_BRIGHTNESS_STEP in params:
436  brightness += params.pop(ATTR_BRIGHTNESS_STEP)
437 
438  else:
439  brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
440 
441  params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
442 
443  preprocess_turn_on_alternatives(hass, params)
444 
445  if (not params or not light.is_on) or (
446  params and ATTR_TRANSITION not in params
447  ):
448  profiles.apply_default(light.entity_id, light.is_on, params)
449 
450  legacy_supported_color_modes = light._light_internal_supported_color_modes # noqa: SLF001
451  supported_color_modes = light.supported_color_modes
452 
453  # If a color temperature is specified, emulate it if not supported by the light
454  if ATTR_COLOR_TEMP_KELVIN in params:
455  if (
456  supported_color_modes
457  and ColorMode.COLOR_TEMP not in supported_color_modes
458  and ColorMode.RGBWW in supported_color_modes
459  ):
460  params.pop(ATTR_COLOR_TEMP)
461  color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
462  brightness = params.get(ATTR_BRIGHTNESS, light.brightness)
463  params[ATTR_RGBWW_COLOR] = color_util.color_temperature_to_rgbww(
464  color_temp,
465  brightness,
466  light.min_color_temp_kelvin,
467  light.max_color_temp_kelvin,
468  )
469  elif ColorMode.COLOR_TEMP not in legacy_supported_color_modes:
470  params.pop(ATTR_COLOR_TEMP)
471  color_temp = params.pop(ATTR_COLOR_TEMP_KELVIN)
472  if color_supported(legacy_supported_color_modes):
473  params[ATTR_HS_COLOR] = color_util.color_temperature_to_hs(
474  color_temp
475  )
476 
477  # If a color is specified, convert to the color space supported by the light
478  # Backwards compatibility: Fall back to hs color if light.supported_color_modes
479  # is not implemented
480  rgb_color: tuple[int, int, int] | None
481  rgbww_color: tuple[int, int, int, int, int] | None
482  if not supported_color_modes:
483  if (rgb_color := params.pop(ATTR_RGB_COLOR, None)) is not None:
484  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
485  elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None:
486  params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
487  elif (rgbw_color := params.pop(ATTR_RGBW_COLOR, None)) is not None:
488  rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
489  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
490  elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None:
491  # https://github.com/python/mypy/issues/13673
492  rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg]
493  *rgbww_color,
494  light.min_color_temp_kelvin,
495  light.max_color_temp_kelvin,
496  )
497  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
498  elif ATTR_HS_COLOR in params and ColorMode.HS not in supported_color_modes:
499  hs_color = params.pop(ATTR_HS_COLOR)
500  if ColorMode.RGB in supported_color_modes:
501  params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
502  elif ColorMode.RGBW in supported_color_modes:
503  rgb_color = color_util.color_hs_to_RGB(*hs_color)
504  params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color)
505  elif ColorMode.RGBWW in supported_color_modes:
506  rgb_color = color_util.color_hs_to_RGB(*hs_color)
507  params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
508  *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
509  )
510  elif ColorMode.XY in supported_color_modes:
511  params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
512  elif ColorMode.COLOR_TEMP in supported_color_modes:
513  xy_color = color_util.color_hs_to_xy(*hs_color)
514  params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
515  *xy_color
516  )
517  params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
518  params[ATTR_COLOR_TEMP_KELVIN]
519  )
520  elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes:
521  rgb_color = params.pop(ATTR_RGB_COLOR)
522  assert rgb_color is not None
523  if ColorMode.RGBW in supported_color_modes:
524  params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color)
525  elif ColorMode.RGBWW in supported_color_modes:
526  params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
527  *rgb_color,
528  light.min_color_temp_kelvin,
529  light.max_color_temp_kelvin,
530  )
531  elif ColorMode.HS in supported_color_modes:
532  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
533  elif ColorMode.XY in supported_color_modes:
534  params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
535  elif ColorMode.COLOR_TEMP in supported_color_modes:
536  xy_color = color_util.color_RGB_to_xy(*rgb_color)
537  params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
538  *xy_color
539  )
540  params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
541  params[ATTR_COLOR_TEMP_KELVIN]
542  )
543  elif ATTR_XY_COLOR in params and ColorMode.XY not in supported_color_modes:
544  xy_color = params.pop(ATTR_XY_COLOR)
545  if ColorMode.HS in supported_color_modes:
546  params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
547  elif ColorMode.RGB in supported_color_modes:
548  params[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color)
549  elif ColorMode.RGBW in supported_color_modes:
550  rgb_color = color_util.color_xy_to_RGB(*xy_color)
551  params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color)
552  elif ColorMode.RGBWW in supported_color_modes:
553  rgb_color = color_util.color_xy_to_RGB(*xy_color)
554  params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
555  *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
556  )
557  elif ColorMode.COLOR_TEMP in supported_color_modes:
558  params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
559  *xy_color
560  )
561  params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
562  params[ATTR_COLOR_TEMP_KELVIN]
563  )
564  elif ATTR_RGBW_COLOR in params and ColorMode.RGBW not in supported_color_modes:
565  rgbw_color = params.pop(ATTR_RGBW_COLOR)
566  rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
567  if ColorMode.RGB in supported_color_modes:
568  params[ATTR_RGB_COLOR] = rgb_color
569  elif ColorMode.RGBWW in supported_color_modes:
570  params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww(
571  *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
572  )
573  elif ColorMode.HS in supported_color_modes:
574  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
575  elif ColorMode.XY in supported_color_modes:
576  params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
577  elif ColorMode.COLOR_TEMP in supported_color_modes:
578  xy_color = color_util.color_RGB_to_xy(*rgb_color)
579  params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
580  *xy_color
581  )
582  params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
583  params[ATTR_COLOR_TEMP_KELVIN]
584  )
585  elif (
586  ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes
587  ):
588  rgbww_color = params.pop(ATTR_RGBWW_COLOR)
589  assert rgbww_color is not None
590  rgb_color = color_util.color_rgbww_to_rgb(
591  *rgbww_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin
592  )
593  if ColorMode.RGB in supported_color_modes:
594  params[ATTR_RGB_COLOR] = rgb_color
595  elif ColorMode.RGBW in supported_color_modes:
596  params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color)
597  elif ColorMode.HS in supported_color_modes:
598  params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
599  elif ColorMode.XY in supported_color_modes:
600  params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
601  elif ColorMode.COLOR_TEMP in supported_color_modes:
602  xy_color = color_util.color_RGB_to_xy(*rgb_color)
603  params[ATTR_COLOR_TEMP_KELVIN] = color_util.color_xy_to_temperature(
604  *xy_color
605  )
606  params[ATTR_COLOR_TEMP] = color_util.color_temperature_kelvin_to_mired(
607  params[ATTR_COLOR_TEMP_KELVIN]
608  )
609 
610  # If white is set to True, set it to the light's brightness
611  # Add a warning in Home Assistant Core 2024.3 if the brightness is set to an
612  # integer.
613  if params.get(ATTR_WHITE) is True:
614  params[ATTR_WHITE] = light.brightness
615 
616  # If both white and brightness are specified, override white
617  if (
618  supported_color_modes
619  and ATTR_WHITE in params
620  and ColorMode.WHITE in supported_color_modes
621  ):
622  params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE])
623 
624  # Remove deprecated white value if the light supports color mode
625  if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:
626  await async_handle_light_off_service(light, call)
627  else:
628  await light.async_turn_on(**filter_turn_on_params(light, params))
629 
630  async def async_handle_light_off_service(
631  light: LightEntity, call: ServiceCall
632  ) -> None:
633  """Handle turning off a light."""
634  params = dict(call.data["params"])
635 
636  if ATTR_TRANSITION not in params:
637  profiles.apply_default(light.entity_id, True, params)
638 
639  await light.async_turn_off(**filter_turn_off_params(light, params))
640 
641  async def async_handle_toggle_service(
642  light: LightEntity, call: ServiceCall
643  ) -> None:
644  """Handle toggling a light."""
645  if light.is_on:
646  await async_handle_light_off_service(light, call)
647  else:
648  await async_handle_light_on_service(light, call)
649 
650  # Listen for light on and light off service calls.
651 
652  component.async_register_entity_service(
653  SERVICE_TURN_ON,
654  vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data),
655  async_handle_light_on_service,
656  )
657 
658  component.async_register_entity_service(
659  SERVICE_TURN_OFF,
660  vol.All(cv.make_entity_service_schema(LIGHT_TURN_OFF_SCHEMA), preprocess_data),
661  async_handle_light_off_service,
662  )
663 
664  component.async_register_entity_service(
665  SERVICE_TOGGLE,
666  vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data),
667  async_handle_toggle_service,
668  )
669 
670  return True
671 
672 
673 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
674  """Set up a config entry."""
675  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
676 
677 
678 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
679  """Unload a config entry."""
680  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
681 
682 
683 def _coerce_none(value: str) -> None:
684  """Coerce an empty string as None."""
685 
686  if not isinstance(value, str):
687  raise vol.Invalid("Expected a string")
688 
689  if value:
690  raise vol.Invalid("Not an empty string")
691 
692 
693 @dataclasses.dataclass
694 class Profile:
695  """Representation of a profile.
696 
697  The light profiles feature is in a frozen development state
698  until otherwise decided in an architecture discussion.
699  """
700 
701  name: str
702  color_x: float | None = dataclasses.field(repr=False)
703  color_y: float | None = dataclasses.field(repr=False)
704  brightness: int | None
705  transition: int | None = None
706  hs_color: tuple[float, float] | None = dataclasses.field(init=False)
707 
708  SCHEMA = vol.Schema(
709  vol.Any(
710  vol.ExactSequence(
711  (
712  str,
713  vol.Any(cv.small_float, _coerce_none),
714  vol.Any(cv.small_float, _coerce_none),
715  vol.Any(cv.byte, _coerce_none),
716  )
717  ),
718  vol.ExactSequence(
719  (
720  str,
721  vol.Any(cv.small_float, _coerce_none),
722  vol.Any(cv.small_float, _coerce_none),
723  vol.Any(cv.byte, _coerce_none),
724  vol.Any(VALID_TRANSITION, _coerce_none),
725  )
726  ),
727  )
728  )
729 
730  def __post_init__(self) -> None:
731  """Convert xy to hs color."""
732  if None in (self.color_x, self.color_y):
733  self.hs_colorhs_color = None
734  return
735 
736  self.hs_colorhs_color = color_util.color_xy_to_hs(
737  cast(float, self.color_x), cast(float, self.color_y)
738  )
739 
740  @classmethod
741  def from_csv_row(cls, csv_row: list[str]) -> Self:
742  """Create profile from a CSV row tuple."""
743  return cls(*cls.SCHEMASCHEMA(csv_row))
744 
745 
746 class Profiles:
747  """Representation of available color profiles.
748 
749  The light profiles feature is in a frozen development state
750  until otherwise decided in an architecture discussion.
751  """
752 
753  def __init__(self, hass: HomeAssistant) -> None:
754  """Initialize profiles."""
755  self.hasshass = hass
756  self.datadata: dict[str, Profile] = {}
757 
758  def _load_profile_data(self) -> dict[str, Profile]:
759  """Load built-in profiles and custom profiles."""
760  profile_paths = [
761  os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE),
762  self.hasshass.config.path(LIGHT_PROFILES_FILE),
763  ]
764  profiles = {}
765 
766  for profile_path in profile_paths:
767  if not os.path.isfile(profile_path):
768  continue
769  with open(profile_path, encoding="utf8") as inp:
770  reader = csv.reader(inp)
771 
772  # Skip the header
773  next(reader, None)
774 
775  try:
776  for rec in reader:
777  profile = Profile.from_csv_row(rec)
778  profiles[profile.name] = profile
779 
780  except vol.MultipleInvalid as ex:
781  _LOGGER.error(
782  "Error parsing light profile row '%s' from %s: %s",
783  rec,
784  profile_path,
785  ex,
786  )
787  continue
788  return profiles
789 
790  async def async_initialize(self) -> None:
791  """Load and cache profiles."""
792  self.datadata = await self.hasshass.async_add_executor_job(self._load_profile_data_load_profile_data)
793 
794  @callback
796  self, entity_id: str, state_on: bool | None, params: dict[str, Any]
797  ) -> None:
798  """Return the default profile for the given light."""
799  for _entity_id in (entity_id, "group.all_lights"):
800  name = f"{_entity_id}.default"
801  if name in self.datadata:
802  if not state_on or not params:
803  self.apply_profileapply_profile(name, params)
804  elif self.datadata[name].transition is not None:
805  params.setdefault(ATTR_TRANSITION, self.datadata[name].transition)
806 
807  @callback
808  def apply_profile(self, name: str, params: dict[str, Any]) -> None:
809  """Apply a profile."""
810  if (profile := self.datadata.get(name)) is None:
811  return
812 
813  color_attributes = (
814  ATTR_COLOR_NAME,
815  ATTR_COLOR_TEMP,
816  ATTR_HS_COLOR,
817  ATTR_RGB_COLOR,
818  ATTR_RGBW_COLOR,
819  ATTR_RGBWW_COLOR,
820  ATTR_XY_COLOR,
821  ATTR_WHITE,
822  )
823 
824  if profile.hs_color is not None and not any(
825  color_attribute in params for color_attribute in color_attributes
826  ):
827  params[ATTR_HS_COLOR] = profile.hs_color
828  if profile.brightness is not None:
829  params.setdefault(ATTR_BRIGHTNESS, profile.brightness)
830  if profile.transition is not None:
831  params.setdefault(ATTR_TRANSITION, profile.transition)
832 
833 
834 class LightEntityDescription(ToggleEntityDescription, frozen_or_thawed=True):
835  """A class that describes binary sensor entities."""
836 
837 
838 CACHED_PROPERTIES_WITH_ATTR_ = {
839  "brightness",
840  "color_mode",
841  "hs_color",
842  "xy_color",
843  "rgb_color",
844  "rgbw_color",
845  "rgbww_color",
846  "color_temp",
847  "min_mireds",
848  "max_mireds",
849  "effect_list",
850  "effect",
851  "supported_color_modes",
852  "supported_features",
853 }
854 
855 
856 class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
857  """Base class for light entities."""
858 
859  _entity_component_unrecorded_attributes = frozenset(
860  {
861  ATTR_SUPPORTED_COLOR_MODES,
862  ATTR_EFFECT_LIST,
863  ATTR_MIN_MIREDS,
864  ATTR_MAX_MIREDS,
865  ATTR_MIN_COLOR_TEMP_KELVIN,
866  ATTR_MAX_COLOR_TEMP_KELVIN,
867  ATTR_BRIGHTNESS,
868  ATTR_COLOR_MODE,
869  ATTR_COLOR_TEMP,
870  ATTR_COLOR_TEMP_KELVIN,
871  ATTR_EFFECT,
872  ATTR_HS_COLOR,
873  ATTR_RGB_COLOR,
874  ATTR_RGBW_COLOR,
875  ATTR_RGBWW_COLOR,
876  ATTR_XY_COLOR,
877  }
878  )
879 
880  entity_description: LightEntityDescription
881  _attr_brightness: int | None = None
882  _attr_color_mode: ColorMode | str | None = None
883  _attr_color_temp: int | None = None
884  _attr_color_temp_kelvin: int | None = None
885  _attr_effect_list: list[str] | None = None
886  _attr_effect: str | None = None
887  _attr_hs_color: tuple[float, float] | None = None
888  # Default to the Philips Hue value that HA has always assumed
889  # https://developers.meethue.com/documentation/core-concepts
890  _attr_max_color_temp_kelvin: int | None = None
891  _attr_min_color_temp_kelvin: int | None = None
892  _attr_max_mireds: int = 500 # 2000 K
893  _attr_min_mireds: int = 153 # 6500 K
894  _attr_rgb_color: tuple[int, int, int] | None = None
895  _attr_rgbw_color: tuple[int, int, int, int] | None = None
896  _attr_rgbww_color: tuple[int, int, int, int, int] | None = None
897  _attr_supported_color_modes: set[ColorMode] | set[str] | None = None
898  _attr_supported_features: LightEntityFeature = LightEntityFeature(0)
899  _attr_xy_color: tuple[float, float] | None = None
900 
901  __color_mode_reported = False
902 
903  @cached_property
904  def brightness(self) -> int | None:
905  """Return the brightness of this light between 0..255."""
906  return self._attr_brightness
907 
908  @cached_property
909  def color_mode(self) -> ColorMode | str | None:
910  """Return the color mode of the light."""
911  return self._attr_color_mode
912 
913  @property
914  def _light_internal_color_mode(self) -> str:
915  """Return the color mode of the light with backwards compatibility."""
916  if (color_mode := self.color_modecolor_mode) is None:
917  # Backwards compatibility for color_mode added in 2021.4
918  # Warning added in 2024.3, break in 2025.3
919  if not self.__color_mode_reported__color_mode_reported__color_mode_reported and self.__should_report_light_issue__should_report_light_issue():
920  self.__color_mode_reported__color_mode_reported__color_mode_reported = True
921  report_issue = self._suggest_report_issue_suggest_report_issue()
922  _LOGGER.warning(
923  (
924  "%s (%s) does not report a color mode, this will stop working "
925  "in Home Assistant Core 2025.3, please %s"
926  ),
927  self.entity_identity_id,
928  type(self),
929  report_issue,
930  )
931 
932  supported = self._light_internal_supported_color_modes_light_internal_supported_color_modes
933 
934  if ColorMode.HS in supported and self.hs_colorhs_color is not None:
935  return ColorMode.HS
936  if ColorMode.COLOR_TEMP in supported and self.color_temp_kelvincolor_temp_kelvin is not None:
937  return ColorMode.COLOR_TEMP
938  if ColorMode.BRIGHTNESS in supported and self.brightnessbrightness is not None:
939  return ColorMode.BRIGHTNESS
940  if ColorMode.ONOFF in supported:
941  return ColorMode.ONOFF
942  return ColorMode.UNKNOWN
943 
944  return color_mode
945 
946  @cached_property
947  def hs_color(self) -> tuple[float, float] | None:
948  """Return the hue and saturation color value [float, float]."""
949  return self._attr_hs_color
950 
951  @cached_property
952  def xy_color(self) -> tuple[float, float] | None:
953  """Return the xy color value [float, float]."""
954  return self._attr_xy_color
955 
956  @cached_property
957  def rgb_color(self) -> tuple[int, int, int] | None:
958  """Return the rgb color value [int, int, int]."""
959  return self._attr_rgb_color
960 
961  @cached_property
962  def rgbw_color(self) -> tuple[int, int, int, int] | None:
963  """Return the rgbw color value [int, int, int, int]."""
964  return self._attr_rgbw_color
965 
966  @property
967  def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None:
968  """Return the rgbw color value [int, int, int, int]."""
969  return self.rgbw_colorrgbw_color
970 
971  @cached_property
972  def rgbww_color(self) -> tuple[int, int, int, int, int] | None:
973  """Return the rgbww color value [int, int, int, int, int]."""
974  return self._attr_rgbww_color
975 
976  @cached_property
977  def color_temp(self) -> int | None:
978  """Return the CT color value in mireds."""
979  return self._attr_color_temp
980 
981  @property
982  def color_temp_kelvin(self) -> int | None:
983  """Return the CT color value in Kelvin."""
984  if self._attr_color_temp_kelvin is None and (color_temp := self.color_tempcolor_temp):
985  return color_util.color_temperature_mired_to_kelvin(color_temp)
986  return self._attr_color_temp_kelvin
987 
988  @cached_property
989  def min_mireds(self) -> int:
990  """Return the coldest color_temp that this light supports."""
991  return self._attr_min_mireds
992 
993  @cached_property
994  def max_mireds(self) -> int:
995  """Return the warmest color_temp that this light supports."""
996  return self._attr_max_mireds
997 
998  @property
999  def min_color_temp_kelvin(self) -> int:
1000  """Return the warmest color_temp_kelvin that this light supports."""
1001  if self._attr_min_color_temp_kelvin is None:
1002  return color_util.color_temperature_mired_to_kelvin(self.max_miredsmax_mireds)
1003  return self._attr_min_color_temp_kelvin
1004 
1005  @property
1006  def max_color_temp_kelvin(self) -> int:
1007  """Return the coldest color_temp_kelvin that this light supports."""
1008  if self._attr_max_color_temp_kelvin is None:
1009  return color_util.color_temperature_mired_to_kelvin(self.min_miredsmin_mireds)
1010  return self._attr_max_color_temp_kelvin
1011 
1012  @cached_property
1013  def effect_list(self) -> list[str] | None:
1014  """Return the list of supported effects."""
1015  return self._attr_effect_list
1016 
1017  @cached_property
1018  def effect(self) -> str | None:
1019  """Return the current effect."""
1020  return self._attr_effect
1021 
1022  @property
1023  def capability_attributes(self) -> dict[str, Any]:
1024  """Return capability attributes."""
1025  data: dict[str, Any] = {}
1026  supported_features = self.supported_features_compatsupported_features_compat
1027  supported_color_modes = self._light_internal_supported_color_modes_light_internal_supported_color_modes
1028 
1029  if ColorMode.COLOR_TEMP in supported_color_modes:
1030  min_color_temp_kelvin = self.min_color_temp_kelvinmin_color_temp_kelvin
1031  max_color_temp_kelvin = self.max_color_temp_kelvinmax_color_temp_kelvin
1032  data[ATTR_MIN_COLOR_TEMP_KELVIN] = min_color_temp_kelvin
1033  data[ATTR_MAX_COLOR_TEMP_KELVIN] = max_color_temp_kelvin
1034  if not max_color_temp_kelvin:
1035  data[ATTR_MIN_MIREDS] = None
1036  else:
1037  data[ATTR_MIN_MIREDS] = color_util.color_temperature_kelvin_to_mired(
1038  max_color_temp_kelvin
1039  )
1040  if not min_color_temp_kelvin:
1041  data[ATTR_MAX_MIREDS] = None
1042  else:
1043  data[ATTR_MAX_MIREDS] = color_util.color_temperature_kelvin_to_mired(
1044  min_color_temp_kelvin
1045  )
1046  if LightEntityFeature.EFFECT in supported_features:
1047  data[ATTR_EFFECT_LIST] = self.effect_listeffect_list
1048 
1049  data[ATTR_SUPPORTED_COLOR_MODES] = sorted(supported_color_modes)
1050 
1051  return data
1052 
1054  self, color_mode: ColorMode | str
1055  ) -> dict[str, tuple[float, ...]]:
1056  data: dict[str, tuple[float, ...]] = {}
1057  if color_mode == ColorMode.HS and (hs_color := self.hs_colorhs_color):
1058  data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3))
1059  data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
1060  data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
1061  elif color_mode == ColorMode.XY and (xy_color := self.xy_colorxy_color):
1062  data[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color)
1063  data[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color)
1064  data[ATTR_XY_COLOR] = (round(xy_color[0], 6), round(xy_color[1], 6))
1065  elif color_mode == ColorMode.RGB and (rgb_color := self.rgb_colorrgb_color):
1066  data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
1067  data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3])
1068  data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
1069  elif color_mode == ColorMode.RGBW and (
1070  rgbw_color := self._light_internal_rgbw_color_light_internal_rgbw_color
1071  ):
1072  rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color)
1073  data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
1074  data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3])
1075  data[ATTR_RGBW_COLOR] = tuple(int(x) for x in rgbw_color[0:4])
1076  data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
1077  elif color_mode == ColorMode.RGBWW and (rgbww_color := self.rgbww_colorrgbww_color):
1078  rgb_color = color_util.color_rgbww_to_rgb(
1079  *rgbww_color, self.min_color_temp_kelvinmin_color_temp_kelvin, self.max_color_temp_kelvinmax_color_temp_kelvin
1080  )
1081  data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color)
1082  data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3])
1083  data[ATTR_RGBWW_COLOR] = tuple(int(x) for x in rgbww_color[0:5])
1084  data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color)
1085  elif color_mode == ColorMode.COLOR_TEMP and (
1086  color_temp_kelvin := self.color_temp_kelvincolor_temp_kelvin
1087  ):
1088  hs_color = color_util.color_temperature_to_hs(color_temp_kelvin)
1089  data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3))
1090  data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color)
1091  data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color)
1092  return data
1093 
1095  self,
1096  color_mode: ColorMode | str | None,
1097  supported_color_modes: set[ColorMode] | set[str],
1098  effect: str | None,
1099  ) -> None:
1100  """Validate the color mode."""
1101  if color_mode is None or color_mode == ColorMode.UNKNOWN:
1102  # The light is turned off or in an unknown state
1103  return
1104 
1105  if not effect or effect == EFFECT_OFF:
1106  # No effect is active, the light must set color mode to one of the supported
1107  # color modes
1108  if color_mode in supported_color_modes:
1109  return
1110  # Warning added in 2024.3, reject in 2025.3
1111  if not self.__color_mode_reported__color_mode_reported__color_mode_reported and self.__should_report_light_issue__should_report_light_issue():
1112  self.__color_mode_reported__color_mode_reported__color_mode_reported = True
1113  report_issue = self._suggest_report_issue_suggest_report_issue()
1114  _LOGGER.warning(
1115  (
1116  "%s (%s) set to unsupported color mode %s, expected one of %s, "
1117  "this will stop working in Home Assistant Core 2025.3, "
1118  "please %s"
1119  ),
1120  self.entity_identity_id,
1121  type(self),
1122  color_mode,
1123  supported_color_modes,
1124  report_issue,
1125  )
1126  return
1127 
1128  # When an effect is active, the color mode should indicate what adjustments are
1129  # supported by the effect. To make this possible, we allow the light to set its
1130  # color mode to on_off, and to brightness if the light allows adjusting
1131  # brightness, in addition to the otherwise supported color modes.
1132  effect_color_modes = supported_color_modes | {ColorMode.ONOFF}
1133  if brightness_supported(effect_color_modes):
1134  effect_color_modes.add(ColorMode.BRIGHTNESS)
1135 
1136  if color_mode in effect_color_modes:
1137  return
1138 
1139  # Warning added in 2024.3, reject in 2025.3
1140  if not self.__color_mode_reported__color_mode_reported__color_mode_reported and self.__should_report_light_issue__should_report_light_issue():
1141  self.__color_mode_reported__color_mode_reported__color_mode_reported = True
1142  report_issue = self._suggest_report_issue_suggest_report_issue()
1143  _LOGGER.warning(
1144  (
1145  "%s (%s) set to unsupported color mode %s when rendering an effect,"
1146  " expected one of %s, this will stop working in Home Assistant "
1147  "Core 2025.3, please %s"
1148  ),
1149  self.entity_identity_id,
1150  type(self),
1151  color_mode,
1152  effect_color_modes,
1153  report_issue,
1154  )
1155  return
1156 
1158  self,
1159  supported_color_modes: set[ColorMode] | set[str],
1160  ) -> None:
1161  """Validate the supported color modes."""
1162  if self.__color_mode_reported__color_mode_reported__color_mode_reported:
1163  return
1164 
1165  try:
1166  valid_supported_color_modes(supported_color_modes)
1167  except vol.Error:
1168  # Warning added in 2024.3, reject in 2025.3
1169  if not self.__color_mode_reported__color_mode_reported__color_mode_reported and self.__should_report_light_issue__should_report_light_issue():
1170  self.__color_mode_reported__color_mode_reported__color_mode_reported = True
1171  report_issue = self._suggest_report_issue_suggest_report_issue()
1172  _LOGGER.warning(
1173  (
1174  "%s (%s) sets invalid supported color modes %s, this will stop "
1175  "working in Home Assistant Core 2025.3, please %s"
1176  ),
1177  self.entity_identity_id,
1178  type(self),
1179  supported_color_modes,
1180  report_issue,
1181  )
1182 
1183  @final
1184  @property
1185  def state_attributes(self) -> dict[str, Any] | None:
1186  """Return state attributes."""
1187  data: dict[str, Any] = {}
1188  supported_features = self.supported_features_compatsupported_features_compat
1189  supported_color_modes = self.supported_color_modessupported_color_modes
1190  legacy_supported_color_modes = (
1191  supported_color_modes or self._light_internal_supported_color_modes_light_internal_supported_color_modes
1192  )
1193  supported_features_value = supported_features.value
1194  _is_on = self.is_onis_on
1195  color_mode = self._light_internal_color_mode_light_internal_color_mode if _is_on else None
1196 
1197  effect: str | None
1198  if LightEntityFeature.EFFECT in supported_features:
1199  data[ATTR_EFFECT] = effect = self.effecteffect if _is_on else None
1200  else:
1201  effect = None
1202 
1203  self.__validate_color_mode__validate_color_mode(color_mode, legacy_supported_color_modes, effect)
1204 
1205  data[ATTR_COLOR_MODE] = color_mode
1206 
1207  if brightness_supported(supported_color_modes):
1208  if color_mode in COLOR_MODES_BRIGHTNESS:
1209  data[ATTR_BRIGHTNESS] = self.brightnessbrightness
1210  else:
1211  data[ATTR_BRIGHTNESS] = None
1212  elif supported_features_value & SUPPORT_BRIGHTNESS:
1213  # Backwards compatibility for ambiguous / incomplete states
1214  # Warning is printed by supported_features_compat, remove in 2025.1
1215  if _is_on:
1216  data[ATTR_BRIGHTNESS] = self.brightnessbrightness
1217  else:
1218  data[ATTR_BRIGHTNESS] = None
1219 
1220  if color_temp_supported(supported_color_modes):
1221  if color_mode == ColorMode.COLOR_TEMP:
1222  color_temp_kelvin = self.color_temp_kelvincolor_temp_kelvin
1223  data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
1224  if color_temp_kelvin:
1225  data[ATTR_COLOR_TEMP] = (
1226  color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)
1227  )
1228  else:
1229  data[ATTR_COLOR_TEMP] = None
1230  else:
1231  data[ATTR_COLOR_TEMP_KELVIN] = None
1232  data[ATTR_COLOR_TEMP] = None
1233  elif supported_features_value & SUPPORT_COLOR_TEMP:
1234  # Backwards compatibility
1235  # Warning is printed by supported_features_compat, remove in 2025.1
1236  if _is_on:
1237  color_temp_kelvin = self.color_temp_kelvincolor_temp_kelvin
1238  data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
1239  if color_temp_kelvin:
1240  data[ATTR_COLOR_TEMP] = (
1241  color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)
1242  )
1243  else:
1244  data[ATTR_COLOR_TEMP] = None
1245  else:
1246  data[ATTR_COLOR_TEMP_KELVIN] = None
1247  data[ATTR_COLOR_TEMP] = None
1248 
1249  if color_supported(legacy_supported_color_modes) or color_temp_supported(
1250  legacy_supported_color_modes
1251  ):
1252  data[ATTR_HS_COLOR] = None
1253  data[ATTR_RGB_COLOR] = None
1254  data[ATTR_XY_COLOR] = None
1255  if ColorMode.RGBW in legacy_supported_color_modes:
1256  data[ATTR_RGBW_COLOR] = None
1257  if ColorMode.RGBWW in legacy_supported_color_modes:
1258  data[ATTR_RGBWW_COLOR] = None
1259  if color_mode:
1260  data.update(self._light_internal_convert_color_light_internal_convert_color(color_mode))
1261 
1262  return data
1263 
1264  @property
1265  def _light_internal_supported_color_modes(self) -> set[ColorMode] | set[str]:
1266  """Calculate supported color modes with backwards compatibility."""
1267  if (_supported_color_modes := self.supported_color_modessupported_color_modes) is not None:
1268  self.__validate_supported_color_modes__validate_supported_color_modes(_supported_color_modes)
1269  return _supported_color_modes
1270 
1271  # Backwards compatibility for supported_color_modes added in 2021.4
1272  # Warning added in 2024.3, remove in 2025.3
1273  if not self.__color_mode_reported__color_mode_reported__color_mode_reported and self.__should_report_light_issue__should_report_light_issue():
1274  self.__color_mode_reported__color_mode_reported__color_mode_reported = True
1275  report_issue = self._suggest_report_issue_suggest_report_issue()
1276  _LOGGER.warning(
1277  (
1278  "%s (%s) does not set supported color modes, this will stop working"
1279  " in Home Assistant Core 2025.3, please %s"
1280  ),
1281  self.entity_identity_id,
1282  type(self),
1283  report_issue,
1284  )
1285  supported_features = self.supported_features_compatsupported_features_compat
1286  supported_features_value = supported_features.value
1287  supported_color_modes: set[ColorMode] = set()
1288 
1289  if supported_features_value & SUPPORT_COLOR_TEMP:
1290  supported_color_modes.add(ColorMode.COLOR_TEMP)
1291  if supported_features_value & SUPPORT_COLOR:
1292  supported_color_modes.add(ColorMode.HS)
1293  if not supported_color_modes and supported_features_value & SUPPORT_BRIGHTNESS:
1294  supported_color_modes = {ColorMode.BRIGHTNESS}
1295 
1296  if not supported_color_modes:
1297  supported_color_modes = {ColorMode.ONOFF}
1298 
1299  return supported_color_modes
1300 
1301  @cached_property
1302  def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
1303  """Flag supported color modes."""
1304  return self._attr_supported_color_modes
1305 
1306  @cached_property
1307  def supported_features(self) -> LightEntityFeature:
1308  """Flag supported features."""
1309  return self._attr_supported_features
1310 
1311  @property
1312  def supported_features_compat(self) -> LightEntityFeature:
1313  """Return the supported features as LightEntityFeature.
1314 
1315  Remove this compatibility shim in 2025.1 or later.
1316  """
1317  features = self.supported_featuressupported_featuressupported_features
1318  if type(features) is not int: # noqa: E721
1319  return features
1320  new_features = LightEntityFeature(features)
1322  return new_features
1324  report_issue = self._suggest_report_issue_suggest_report_issue()
1325  report_issue += (
1326  " and reference "
1327  "https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation"
1328  )
1329  _LOGGER.warning(
1330  (
1331  "Entity %s (%s) is using deprecated supported features"
1332  " values which will be removed in HA Core 2025.1. Instead it should use"
1333  " %s and color modes, please %s"
1334  ),
1335  self.entity_identity_id,
1336  type(self),
1337  repr(new_features),
1338  report_issue,
1339  )
1340  return new_features
1341 
1342  def __should_report_light_issue(self) -> bool:
1343  """Return if light color mode issues should be reported."""
1344  if not self.platformplatform:
1345  return True
1346  # philips_js has known issues, we don't need users to open issues
1347  return self.platformplatform.platform_name not in {"philips_js"}
tuple[int, int, int, int]|None _light_internal_rgbw_color(self)
Definition: __init__.py:967
tuple[int, int, int]|None rgb_color(self)
Definition: __init__.py:957
dict[str, tuple[float,...]] _light_internal_convert_color(self, ColorMode|str color_mode)
Definition: __init__.py:1055
tuple[int, int, int, int]|None rgbw_color(self)
Definition: __init__.py:962
tuple[int, int, int, int, int]|None rgbww_color(self)
Definition: __init__.py:972
set[ColorMode]|set[str] _light_internal_supported_color_modes(self)
Definition: __init__.py:1265
dict[str, Any] capability_attributes(self)
Definition: __init__.py:1023
dict[str, Any]|None state_attributes(self)
Definition: __init__.py:1185
set[ColorMode]|set[str]|None supported_color_modes(self)
Definition: __init__.py:1302
None __validate_color_mode(self, ColorMode|str|None color_mode, set[ColorMode]|set[str] supported_color_modes, str|None effect)
Definition: __init__.py:1099
tuple[float, float]|None hs_color(self)
Definition: __init__.py:947
None __validate_supported_color_modes(self, set[ColorMode]|set[str] supported_color_modes)
Definition: __init__.py:1160
LightEntityFeature supported_features_compat(self)
Definition: __init__.py:1312
ColorMode|str|None color_mode(self)
Definition: __init__.py:909
tuple[float, float]|None xy_color(self)
Definition: __init__.py:952
LightEntityFeature supported_features(self)
Definition: __init__.py:1307
Self from_csv_row(cls, list[str] csv_row)
Definition: __init__.py:741
None apply_default(self, str entity_id, bool|None state_on, dict[str, Any] params)
Definition: __init__.py:797
None __init__(self, HomeAssistant hass)
Definition: __init__.py:753
None apply_profile(self, str name, dict[str, Any] params)
Definition: __init__.py:808
dict[str, Profile] _load_profile_data(self)
Definition: __init__.py:758
int|None supported_features(self)
Definition: entity.py:861
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool color_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:162
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:396
set[ColorMode|str] valid_supported_color_modes(Iterable[ColorMode|str] color_modes)
Definition: __init__.py:141
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
Definition: __init__.py:122
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:678
dict[str, Any] filter_turn_on_params(LightEntity light, dict[str, Any] params)
Definition: __init__.py:361
None preprocess_turn_on_alternatives(HomeAssistant hass, dict[str, Any] params)
Definition: __init__.py:305
set[str]|None get_supported_color_modes(HomeAssistant hass, str entity_id)
Definition: __init__.py:176
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:673
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:155
dict[str, Any] filter_turn_off_params(LightEntity light, dict[str, Any] params)
Definition: __init__.py:346
bool color_temp_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:169
None _coerce_none(str value)
Definition: __init__.py:683
bool is_on(HomeAssistant hass, str entity_id)
Definition: __init__.py:298
None open(self, **Any kwargs)
Definition: lock.py:86