Home Assistant Unofficial Reference 2024.12.1
schema_json.py
Go to the documentation of this file.
1 """Support for MQTT JSON lights."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from contextlib import suppress
7 import logging
8 from typing import TYPE_CHECKING, Any, cast
9 
10 import voluptuous as vol
11 
13  ATTR_BRIGHTNESS,
14  ATTR_COLOR_MODE,
15  ATTR_COLOR_TEMP,
16  ATTR_EFFECT,
17  ATTR_FLASH,
18  ATTR_HS_COLOR,
19  ATTR_RGB_COLOR,
20  ATTR_RGBW_COLOR,
21  ATTR_RGBWW_COLOR,
22  ATTR_TRANSITION,
23  ATTR_WHITE,
24  ATTR_XY_COLOR,
25  DOMAIN as LIGHT_DOMAIN,
26  ENTITY_ID_FORMAT,
27  FLASH_LONG,
28  FLASH_SHORT,
29  VALID_COLOR_MODES,
30  ColorMode,
31  LightEntity,
32  LightEntityFeature,
33  brightness_supported,
34  color_supported,
35  filter_supported_color_modes,
36  valid_supported_color_modes,
37 )
38 from homeassistant.const import (
39  CONF_BRIGHTNESS,
40  CONF_COLOR_TEMP,
41  CONF_EFFECT,
42  CONF_HS,
43  CONF_NAME,
44  CONF_OPTIMISTIC,
45  CONF_RGB,
46  CONF_XY,
47  STATE_ON,
48 )
49 from homeassistant.core import async_get_hass, callback
51 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
52 from homeassistant.helpers.json import json_dumps
53 from homeassistant.helpers.restore_state import RestoreEntity
54 from homeassistant.helpers.typing import ConfigType, VolSchemaType
55 import homeassistant.util.color as color_util
56 from homeassistant.util.json import json_loads_object
57 from homeassistant.util.yaml import dump as yaml_dump
58 
59 from .. import subscription
60 from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA
61 from ..const import (
62  CONF_COMMAND_TOPIC,
63  CONF_QOS,
64  CONF_RETAIN,
65  CONF_STATE_TOPIC,
66  DOMAIN as MQTT_DOMAIN,
67 )
68 from ..entity import MqttEntity
69 from ..models import ReceiveMessage
70 from ..schemas import MQTT_ENTITY_COMMON_SCHEMA
71 from ..util import valid_subscribe_topic
72 from .schema import MQTT_LIGHT_SCHEMA_SCHEMA
73 from .schema_basic import (
74  CONF_BRIGHTNESS_SCALE,
75  CONF_WHITE_SCALE,
76  MQTT_LIGHT_ATTRIBUTES_BLOCKED,
77 )
78 
79 _LOGGER = logging.getLogger(__name__)
80 
81 DOMAIN = "mqtt_json"
82 
83 DEFAULT_BRIGHTNESS = False
84 DEFAULT_COLOR_MODE = False
85 DEFAULT_COLOR_TEMP = False
86 DEFAULT_EFFECT = False
87 DEFAULT_FLASH_TIME_LONG = 10
88 DEFAULT_FLASH_TIME_SHORT = 2
89 DEFAULT_NAME = "MQTT JSON Light"
90 DEFAULT_RGB = False
91 DEFAULT_XY = False
92 DEFAULT_HS = False
93 DEFAULT_BRIGHTNESS_SCALE = 255
94 DEFAULT_WHITE_SCALE = 255
95 
96 CONF_COLOR_MODE = "color_mode"
97 CONF_SUPPORTED_COLOR_MODES = "supported_color_modes"
98 
99 CONF_EFFECT_LIST = "effect_list"
100 
101 CONF_FLASH_TIME_LONG = "flash_time_long"
102 CONF_FLASH_TIME_SHORT = "flash_time_short"
103 
104 CONF_MAX_MIREDS = "max_mireds"
105 CONF_MIN_MIREDS = "min_mireds"
106 
107 
109  setup_from_yaml: bool,
110 ) -> Callable[[dict[str, Any]], dict[str, Any]]:
111  """Test color_mode is not combined with deprecated config."""
112 
113  def _valid_color_configuration(config: ConfigType) -> ConfigType:
114  deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY}
115  deprecated_flags_used = any(config.get(key) for key in deprecated)
116  if config.get(CONF_SUPPORTED_COLOR_MODES):
117  if deprecated_flags_used:
118  raise vol.Invalid(
119  "supported_color_modes must not "
120  f"be combined with any of {deprecated}"
121  )
122  elif deprecated_flags_used:
123  deprecated_flags = ", ".join(key for key in deprecated if key in config)
124  _LOGGER.warning(
125  "Deprecated flags [%s] used in MQTT JSON light config "
126  "for handling color mode, please use `supported_color_modes` instead. "
127  "Got: %s. This will stop working in Home Assistant Core 2025.3",
128  deprecated_flags,
129  config,
130  )
131  if not setup_from_yaml:
132  return config
133  issue_id = hex(hash(frozenset(config)))
134  yaml_config_str = yaml_dump(config)
135  learn_more_url = (
136  "https://www.home-assistant.io/integrations/"
137  f"{LIGHT_DOMAIN}.mqtt/#json-schema"
138  )
139  hass = async_get_hass()
141  hass,
142  MQTT_DOMAIN,
143  issue_id,
144  issue_domain=LIGHT_DOMAIN,
145  is_fixable=False,
146  severity=IssueSeverity.WARNING,
147  learn_more_url=learn_more_url,
148  translation_placeholders={
149  "deprecated_flags": deprecated_flags,
150  "config": yaml_config_str,
151  },
152  translation_key="deprecated_color_handling",
153  )
154 
155  if CONF_COLOR_MODE in config:
156  _LOGGER.warning(
157  "Deprecated flag `color_mode` used in MQTT JSON light config "
158  ", the `color_mode` flag is not used anymore and should be removed. "
159  "Got: %s. This will stop working in Home Assistant Core 2025.3",
160  config,
161  )
162  if not setup_from_yaml:
163  return config
164  issue_id = hex(hash(frozenset(config)))
165  yaml_config_str = yaml_dump(config)
166  learn_more_url = (
167  "https://www.home-assistant.io/integrations/"
168  f"{LIGHT_DOMAIN}.mqtt/#json-schema"
169  )
170  hass = async_get_hass()
172  hass,
173  MQTT_DOMAIN,
174  issue_id,
175  breaks_in_ha_version="2025.3.0",
176  issue_domain=LIGHT_DOMAIN,
177  is_fixable=False,
178  severity=IssueSeverity.WARNING,
179  learn_more_url=learn_more_url,
180  translation_placeholders={
181  "config": yaml_config_str,
182  },
183  translation_key="deprecated_color_mode_flag",
184  )
185 
186  return config
187 
188  return _valid_color_configuration
189 
190 
191 _PLATFORM_SCHEMA_BASE = (
192  MQTT_RW_SCHEMA.extend(
193  {
194  vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
195  vol.Optional(
196  CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE
197  ): vol.All(vol.Coerce(int), vol.Range(min=1)),
198  # CONF_COLOR_MODE was deprecated with HA Core 2024.4 and will be
199  # removed with HA Core 2025.3
200  vol.Optional(CONF_COLOR_MODE): cv.boolean,
201  # CONF_COLOR_TEMP was deprecated with HA Core 2024.4 and will be
202  # removed with HA Core 2025.3
203  vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean,
204  vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean,
205  vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
206  vol.Optional(
207  CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG
208  ): cv.positive_int,
209  vol.Optional(
210  CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT
211  ): cv.positive_int,
212  # CONF_HS was deprecated with HA Core 2024.4 and will be
213  # removed with HA Core 2025.3
214  vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean,
215  vol.Optional(CONF_MAX_MIREDS): cv.positive_int,
216  vol.Optional(CONF_MIN_MIREDS): cv.positive_int,
217  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
218  vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
219  vol.Coerce(int), vol.In([0, 1, 2])
220  ),
221  vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
222  # CONF_RGB was deprecated with HA Core 2024.4 and will be
223  # removed with HA Core 2025.3
224  vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean,
225  vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
226  vol.Optional(CONF_SUPPORTED_COLOR_MODES): vol.All(
227  cv.ensure_list,
228  [vol.In(VALID_COLOR_MODES)],
229  vol.Unique(),
230  valid_supported_color_modes,
231  ),
232  vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All(
233  vol.Coerce(int), vol.Range(min=1)
234  ),
235  # CONF_XY was deprecated with HA Core 2024.4 and will be
236  # removed with HA Core 2025.3
237  vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
238  },
239  )
240  .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
241  .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
242 )
243 
244 DISCOVERY_SCHEMA_JSON = vol.All(
246  _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
247 )
248 
249 PLATFORM_SCHEMA_MODERN_JSON = vol.All(
251  _PLATFORM_SCHEMA_BASE,
252 )
253 
254 
256  """Representation of a MQTT JSON light."""
257 
258  _default_name = DEFAULT_NAME
259  _entity_id_format = ENTITY_ID_FORMAT
260  _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
261 
262  _fixed_color_mode: ColorMode | str | None = None
263  _flash_times: dict[str, int | None]
264  _topic: dict[str, str | None]
265  _optimistic: bool
266 
267  _deprecated_color_handling: bool = False
268 
269  @staticmethod
270  def config_schema() -> VolSchemaType:
271  """Return the config schema."""
272  return DISCOVERY_SCHEMA_JSON
273 
274  def _setup_from_config(self, config: ConfigType) -> None:
275  """(Re)Setup the entity."""
276  self._attr_max_mireds_attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds)
277  self._attr_min_mireds_attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds)
278  self._attr_effect_list_attr_effect_list = config.get(CONF_EFFECT_LIST)
279 
280  self._topic_topic = {
281  key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC)
282  }
283  optimistic: bool = config[CONF_OPTIMISTIC]
284  self._optimistic_optimistic = optimistic or self._topic_topic[CONF_STATE_TOPIC] is None
285  self._attr_assumed_state_attr_assumed_state = bool(self._optimistic_optimistic)
286 
287  self._flash_times_flash_times = {
288  key: config.get(key)
289  for key in (CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG)
290  }
291 
292  self._attr_supported_features_attr_supported_features = (
293  LightEntityFeature.TRANSITION | LightEntityFeature.FLASH
294  )
295  self._attr_supported_features_attr_supported_features |= (
296  config[CONF_EFFECT] and LightEntityFeature.EFFECT
297  )
298  if supported_color_modes := self._config_config.get(CONF_SUPPORTED_COLOR_MODES):
299  self._attr_supported_color_modes_attr_supported_color_modes = supported_color_modes
300  if self.supported_color_modessupported_color_modes and len(self.supported_color_modessupported_color_modes) == 1:
301  self._attr_color_mode_attr_color_mode = next(iter(self.supported_color_modessupported_color_modes))
302  else:
303  self._attr_color_mode_attr_color_mode = ColorMode.UNKNOWN
304  else:
305  self._deprecated_color_handling_deprecated_color_handling = True
306  color_modes = {ColorMode.ONOFF}
307  if config[CONF_BRIGHTNESS]:
308  color_modes.add(ColorMode.BRIGHTNESS)
309  if config[CONF_COLOR_TEMP]:
310  color_modes.add(ColorMode.COLOR_TEMP)
311  if config[CONF_HS] or config[CONF_RGB] or config[CONF_XY]:
312  color_modes.add(ColorMode.HS)
313  self._attr_supported_color_modes_attr_supported_color_modes = filter_supported_color_modes(color_modes)
314  if self.supported_color_modessupported_color_modes and len(self.supported_color_modessupported_color_modes) == 1:
315  self._fixed_color_mode_fixed_color_mode = next(iter(self.supported_color_modessupported_color_modes))
316 
317  def _update_color(self, values: dict[str, Any]) -> None:
318  if self._deprecated_color_handling_deprecated_color_handling:
319  # Deprecated color handling
320  try:
321  red = int(values["color"]["r"])
322  green = int(values["color"]["g"])
323  blue = int(values["color"]["b"])
324  self._attr_hs_color_attr_hs_color = color_util.color_RGB_to_hs(red, green, blue)
325  except KeyError:
326  pass
327  except ValueError:
328  _LOGGER.warning(
329  "Invalid RGB color value '%s' received for entity %s",
330  values,
331  self.entity_identity_id,
332  )
333  return
334 
335  try:
336  x_color = float(values["color"]["x"])
337  y_color = float(values["color"]["y"])
338  self._attr_hs_color_attr_hs_color = color_util.color_xy_to_hs(x_color, y_color)
339  except KeyError:
340  pass
341  except ValueError:
342  _LOGGER.warning(
343  "Invalid XY color value '%s' received for entity %s",
344  values,
345  self.entity_identity_id,
346  )
347  return
348 
349  try:
350  hue = float(values["color"]["h"])
351  saturation = float(values["color"]["s"])
352  self._attr_hs_color_attr_hs_color = (hue, saturation)
353  except KeyError:
354  pass
355  except ValueError:
356  _LOGGER.warning(
357  "Invalid HS color value '%s' received for entity %s",
358  values,
359  self.entity_identity_id,
360  )
361  return
362  else:
363  color_mode: str = values["color_mode"]
364  if not self._supports_color_mode_supports_color_mode(color_mode):
365  _LOGGER.warning(
366  "Invalid color mode '%s' received for entity %s",
367  color_mode,
368  self.entity_identity_id,
369  )
370  return
371  try:
372  if color_mode == ColorMode.COLOR_TEMP:
373  self._attr_color_temp_attr_color_temp = int(values["color_temp"])
374  self._attr_color_mode_attr_color_mode = ColorMode.COLOR_TEMP
375  elif color_mode == ColorMode.HS:
376  hue = float(values["color"]["h"])
377  saturation = float(values["color"]["s"])
378  self._attr_color_mode_attr_color_mode = ColorMode.HS
379  self._attr_hs_color_attr_hs_color = (hue, saturation)
380  elif color_mode == ColorMode.RGB:
381  r = int(values["color"]["r"])
382  g = int(values["color"]["g"])
383  b = int(values["color"]["b"])
384  self._attr_color_mode_attr_color_mode = ColorMode.RGB
385  self._attr_rgb_color_attr_rgb_color = (r, g, b)
386  elif color_mode == ColorMode.RGBW:
387  r = int(values["color"]["r"])
388  g = int(values["color"]["g"])
389  b = int(values["color"]["b"])
390  w = int(values["color"]["w"])
391  self._attr_color_mode_attr_color_mode = ColorMode.RGBW
392  self._attr_rgbw_color_attr_rgbw_color = (r, g, b, w)
393  elif color_mode == ColorMode.RGBWW:
394  r = int(values["color"]["r"])
395  g = int(values["color"]["g"])
396  b = int(values["color"]["b"])
397  c = int(values["color"]["c"])
398  w = int(values["color"]["w"])
399  self._attr_color_mode_attr_color_mode = ColorMode.RGBWW
400  self._attr_rgbww_color_attr_rgbww_color = (r, g, b, c, w)
401  elif color_mode == ColorMode.WHITE:
402  self._attr_color_mode_attr_color_mode = ColorMode.WHITE
403  elif color_mode == ColorMode.XY:
404  x = float(values["color"]["x"])
405  y = float(values["color"]["y"])
406  self._attr_color_mode_attr_color_mode = ColorMode.XY
407  self._attr_xy_color_attr_xy_color = (x, y)
408  except (KeyError, ValueError):
409  _LOGGER.warning(
410  "Invalid or incomplete color value '%s' received for entity %s",
411  values,
412  self.entity_identity_id,
413  )
414 
415  @callback
416  def _state_received(self, msg: ReceiveMessage) -> None:
417  """Handle new MQTT messages."""
418  values = json_loads_object(msg.payload)
419 
420  if values["state"] == "ON":
421  self._attr_is_on_attr_is_on = True
422  elif values["state"] == "OFF":
423  self._attr_is_on_attr_is_on = False
424  elif values["state"] is None:
425  self._attr_is_on_attr_is_on = None
426 
427  if (
428  self._deprecated_color_handling_deprecated_color_handling
429  and color_supported(self.supported_color_modessupported_color_modes)
430  and "color" in values
431  ):
432  # Deprecated color handling
433  if values["color"] is None:
434  self._attr_hs_color_attr_hs_color = None
435  else:
436  self._update_color_update_color(values)
437 
438  if not self._deprecated_color_handling_deprecated_color_handling and "color_mode" in values:
439  self._update_color_update_color(values)
440 
441  if brightness_supported(self.supported_color_modessupported_color_modes):
442  try:
443  if brightness := values["brightness"]:
444  if TYPE_CHECKING:
445  assert isinstance(brightness, float)
446  self._attr_brightness_attr_brightness = color_util.value_to_brightness(
447  (1, self._config_config[CONF_BRIGHTNESS_SCALE]), brightness
448  )
449  else:
450  _LOGGER.debug(
451  "Ignoring zero brightness value for entity %s",
452  self.entity_identity_id,
453  )
454 
455  except KeyError:
456  pass
457  except (TypeError, ValueError):
458  _LOGGER.warning(
459  "Invalid brightness value '%s' received for entity %s",
460  values["brightness"],
461  self.entity_identity_id,
462  )
463 
464  if (
465  self._deprecated_color_handling_deprecated_color_handling
466  and self.supported_color_modessupported_color_modes
467  and ColorMode.COLOR_TEMP in self.supported_color_modessupported_color_modes
468  ):
469  # Deprecated color handling
470  try:
471  if values["color_temp"] is None:
472  self._attr_color_temp_attr_color_temp = None
473  else:
474  self._attr_color_temp_attr_color_temp = int(values["color_temp"]) # type: ignore[arg-type]
475  except KeyError:
476  pass
477  except ValueError:
478  _LOGGER.warning(
479  "Invalid color temp value '%s' received for entity %s",
480  values["color_temp"],
481  self.entity_identity_id,
482  )
483  # Allow to switch back to color_temp
484  if "color" not in values:
485  self._attr_hs_color_attr_hs_color = None
486 
487  if self.supported_featuressupported_featuressupported_features and LightEntityFeature.EFFECT:
488  with suppress(KeyError):
489  self._attr_effect_attr_effect = cast(str, values["effect"])
490 
491  @callback
492  def _prepare_subscribe_topics(self) -> None:
493  """(Re)Subscribe to topics."""
494  self.add_subscriptionadd_subscription(
495  CONF_STATE_TOPIC,
496  self._state_received_state_received,
497  {
498  "_attr_brightness",
499  "_attr_color_temp",
500  "_attr_effect",
501  "_attr_hs_color",
502  "_attr_is_on",
503  "_attr_rgb_color",
504  "_attr_rgbw_color",
505  "_attr_rgbww_color",
506  "_attr_xy_color",
507  "color_mode",
508  },
509  )
510 
511  async def _subscribe_topics(self) -> None:
512  """(Re)Subscribe to topics."""
513  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
514 
515  last_state = await self.async_get_last_stateasync_get_last_state()
516  if self._optimistic_optimistic and last_state:
517  self._attr_is_on_attr_is_on = last_state.state == STATE_ON
518  last_attributes = last_state.attributes
519  self._attr_brightness_attr_brightness = last_attributes.get(
520  ATTR_BRIGHTNESS, self.brightnessbrightness
521  )
522  self._attr_color_mode_attr_color_mode = last_attributes.get(
523  ATTR_COLOR_MODE, self.color_modecolor_modecolor_mode
524  )
525  self._attr_color_temp_attr_color_temp = last_attributes.get(
526  ATTR_COLOR_TEMP, self.color_tempcolor_temp
527  )
528  self._attr_effect_attr_effect = last_attributes.get(ATTR_EFFECT, self.effecteffect)
529  self._attr_hs_color_attr_hs_color = last_attributes.get(ATTR_HS_COLOR, self.hs_colorhs_color)
530  self._attr_rgb_color_attr_rgb_color = last_attributes.get(ATTR_RGB_COLOR, self.rgb_colorrgb_color)
531  self._attr_rgbw_color_attr_rgbw_color = last_attributes.get(
532  ATTR_RGBW_COLOR, self.rgbw_colorrgbw_color
533  )
534  self._attr_rgbww_color_attr_rgbww_color = last_attributes.get(
535  ATTR_RGBWW_COLOR, self.rgbww_colorrgbww_color
536  )
537  self._attr_xy_color_attr_xy_color = last_attributes.get(ATTR_XY_COLOR, self.xy_colorxy_color)
538 
539  @property
540  def color_mode(self) -> ColorMode | str | None:
541  """Return current color mode."""
542  if not self._deprecated_color_handling_deprecated_color_handling:
543  return self._attr_color_mode_attr_color_mode
544  if self._fixed_color_mode_fixed_color_mode:
545  # Legacy light with support for a single color mode
546  return self._fixed_color_mode_fixed_color_mode
547  # Legacy light with support for ct + hs, prioritize hs
548  if self.hs_colorhs_color is not None:
549  return ColorMode.HS
550  return ColorMode.COLOR_TEMP
551 
552  def _set_flash_and_transition(self, message: dict[str, Any], **kwargs: Any) -> None:
553  if ATTR_TRANSITION in kwargs:
554  message["transition"] = kwargs[ATTR_TRANSITION]
555 
556  if ATTR_FLASH in kwargs:
557  flash: str = kwargs[ATTR_FLASH]
558 
559  if flash == FLASH_LONG:
560  message["flash"] = self._flash_times_flash_times[CONF_FLASH_TIME_LONG]
561  elif flash == FLASH_SHORT:
562  message["flash"] = self._flash_times_flash_times[CONF_FLASH_TIME_SHORT]
563 
564  def _scale_rgbxx(self, rgbxx: tuple[int, ...], kwargs: Any) -> tuple[int, ...]:
565  # If brightness is supported, we don't want to scale the
566  # RGBxx values given using the brightness and
567  # we pop the brightness, to omit it from the payload
568  brightness: int
569  if self._config_config[CONF_BRIGHTNESS]:
570  brightness = 255
571  else:
572  brightness = kwargs.pop(ATTR_BRIGHTNESS, 255)
573  return tuple(round(i / 255 * brightness) for i in rgbxx)
574 
575  def _supports_color_mode(self, color_mode: ColorMode | str) -> bool:
576  """Return True if the light natively supports a color mode."""
577  return (
578  not self._deprecated_color_handling_deprecated_color_handling
579  and self.supported_color_modessupported_color_modes is not None
580  and color_mode in self.supported_color_modessupported_color_modes
581  )
582 
583  async def async_turn_on(self, **kwargs: Any) -> None: # noqa: C901
584  """Turn the device on.
585 
586  This method is a coroutine.
587  """
588  brightness: int
589  should_update = False
590  hs_color: tuple[float, float]
591  message: dict[str, Any] = {"state": "ON"}
592  rgb: tuple[int, ...]
593  rgbw: tuple[int, ...]
594  rgbcw: tuple[int, ...]
595  xy_color: tuple[float, float]
596 
597  if ATTR_HS_COLOR in kwargs and (
598  self._config_config[CONF_HS] or self._config_config[CONF_RGB] or self._config_config[CONF_XY]
599  ):
600  # Legacy color handling
601  hs_color = kwargs[ATTR_HS_COLOR]
602  message["color"] = {}
603  if self._config_config[CONF_RGB]:
604  # If brightness is supported, we don't want to scale the
605  # RGB values given using the brightness.
606  if self._config_config[CONF_BRIGHTNESS]:
607  brightness = 255
608  else:
609  # We pop the brightness, to omit it from the payload
610  brightness = kwargs.pop(ATTR_BRIGHTNESS, 255)
611  rgb = color_util.color_hsv_to_RGB(
612  hs_color[0], hs_color[1], brightness / 255 * 100
613  )
614  message["color"]["r"] = rgb[0]
615  message["color"]["g"] = rgb[1]
616  message["color"]["b"] = rgb[2]
617  if self._config_config[CONF_XY]:
618  xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
619  message["color"]["x"] = xy_color[0]
620  message["color"]["y"] = xy_color[1]
621  if self._config_config[CONF_HS]:
622  message["color"]["h"] = hs_color[0]
623  message["color"]["s"] = hs_color[1]
624 
625  if self._optimistic_optimistic:
626  self._attr_color_temp_attr_color_temp = None
627  self._attr_hs_color_attr_hs_color = kwargs[ATTR_HS_COLOR]
628  should_update = True
629 
630  if ATTR_HS_COLOR in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.HS):
631  hs_color = kwargs[ATTR_HS_COLOR]
632  message["color"] = {"h": hs_color[0], "s": hs_color[1]}
633  if self._optimistic_optimistic:
634  self._attr_color_mode_attr_color_mode = ColorMode.HS
635  self._attr_hs_color_attr_hs_color = hs_color
636  should_update = True
637 
638  if ATTR_RGB_COLOR in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.RGB):
639  rgb = self._scale_rgbxx_scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs)
640  message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2]}
641  if self._optimistic_optimistic:
642  self._attr_color_mode_attr_color_mode = ColorMode.RGB
643  self._attr_rgb_color_attr_rgb_color = cast(tuple[int, int, int], rgb)
644  should_update = True
645 
646  if ATTR_RGBW_COLOR in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.RGBW):
647  rgbw = self._scale_rgbxx_scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs)
648  message["color"] = {"r": rgbw[0], "g": rgbw[1], "b": rgbw[2], "w": rgbw[3]}
649  if self._optimistic_optimistic:
650  self._attr_color_mode_attr_color_mode = ColorMode.RGBW
651  self._attr_rgbw_color_attr_rgbw_color = cast(tuple[int, int, int, int], rgbw)
652  should_update = True
653 
654  if ATTR_RGBWW_COLOR in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.RGBWW):
655  rgbcw = self._scale_rgbxx_scale_rgbxx(kwargs[ATTR_RGBWW_COLOR], kwargs)
656  message["color"] = {
657  "r": rgbcw[0],
658  "g": rgbcw[1],
659  "b": rgbcw[2],
660  "c": rgbcw[3],
661  "w": rgbcw[4],
662  }
663  if self._optimistic_optimistic:
664  self._attr_color_mode_attr_color_mode = ColorMode.RGBWW
665  self._attr_rgbww_color_attr_rgbww_color = cast(tuple[int, int, int, int, int], rgbcw)
666  should_update = True
667 
668  if ATTR_XY_COLOR in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.XY):
669  xy_color = kwargs[ATTR_XY_COLOR]
670  message["color"] = {"x": xy_color[0], "y": xy_color[1]}
671  if self._optimistic_optimistic:
672  self._attr_color_mode_attr_color_mode = ColorMode.XY
673  self._attr_xy_color_attr_xy_color = xy_color
674  should_update = True
675 
676  self._set_flash_and_transition_set_flash_and_transition(message, **kwargs)
677 
678  if ATTR_BRIGHTNESS in kwargs and brightness_supported(
679  self.supported_color_modessupported_color_modes
680  ):
681  device_brightness = color_util.brightness_to_value(
682  (1, self._config_config[CONF_BRIGHTNESS_SCALE]),
683  kwargs[ATTR_BRIGHTNESS],
684  )
685  # Make sure the brightness is not rounded down to 0
686  device_brightness = max(round(device_brightness), 1)
687  message["brightness"] = device_brightness
688 
689  if self._optimistic_optimistic:
690  self._attr_brightness_attr_brightness = kwargs[ATTR_BRIGHTNESS]
691  should_update = True
692 
693  if ATTR_COLOR_TEMP in kwargs:
694  message["color_temp"] = int(kwargs[ATTR_COLOR_TEMP])
695 
696  if self._optimistic_optimistic:
697  self._attr_color_mode_attr_color_mode = ColorMode.COLOR_TEMP
698  self._attr_color_temp_attr_color_temp = kwargs[ATTR_COLOR_TEMP]
699  self._attr_hs_color_attr_hs_color = None
700  should_update = True
701 
702  if ATTR_EFFECT in kwargs:
703  message["effect"] = kwargs[ATTR_EFFECT]
704 
705  if self._optimistic_optimistic:
706  self._attr_effect_attr_effect = kwargs[ATTR_EFFECT]
707  should_update = True
708 
709  if ATTR_WHITE in kwargs and self._supports_color_mode_supports_color_mode(ColorMode.WHITE):
710  white_normalized = kwargs[ATTR_WHITE] / DEFAULT_WHITE_SCALE
711  white_scale = self._config_config[CONF_WHITE_SCALE]
712  device_white_level = min(round(white_normalized * white_scale), white_scale)
713  # Make sure the brightness is not rounded down to 0
714  device_white_level = max(device_white_level, 1)
715  message["white"] = device_white_level
716 
717  if self._optimistic_optimistic:
718  self._attr_color_mode_attr_color_mode = ColorMode.WHITE
719  self._attr_brightness_attr_brightness = kwargs[ATTR_WHITE]
720  should_update = True
721 
722  await self.async_publish_with_configasync_publish_with_config(
723  str(self._topic_topic[CONF_COMMAND_TOPIC]), json_dumps(message)
724  )
725 
726  if self._optimistic_optimistic:
727  # Optimistically assume that the light has changed state.
728  self._attr_is_on_attr_is_on = True
729  should_update = True
730 
731  if should_update:
732  self.async_write_ha_stateasync_write_ha_state()
733 
734  async def async_turn_off(self, **kwargs: Any) -> None:
735  """Turn the device off.
736 
737  This method is a coroutine.
738  """
739  message: dict[str, Any] = {"state": "OFF"}
740 
741  self._set_flash_and_transition_set_flash_and_transition(message, **kwargs)
742 
743  await self.async_publish_with_configasync_publish_with_config(
744  str(self._topic_topic[CONF_COMMAND_TOPIC]), json_dumps(message)
745  )
746 
747  if self._optimistic_optimistic:
748  # Optimistically assume that the light has changed state.
749  self._attr_is_on_attr_is_on = False
750  self.async_write_ha_stateasync_write_ha_state()
tuple[int, int, int]|None rgb_color(self)
Definition: __init__.py:957
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]|None supported_color_modes(self)
Definition: __init__.py:1302
tuple[float, float]|None hs_color(self)
Definition: __init__.py:947
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
None async_publish_with_config(self, str topic, PublishPayloadType payload)
Definition: entity.py:1377
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
tuple[int,...] _scale_rgbxx(self, tuple[int,...] rgbxx, Any kwargs)
Definition: schema_json.py:564
None _set_flash_and_transition(self, dict[str, Any] message, **Any kwargs)
Definition: schema_json.py:552
bool _supports_color_mode(self, ColorMode|str color_mode)
Definition: schema_json.py:575
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
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
Definition: __init__.py:122
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Definition: __init__.py:155
Callable[[dict[str, Any]], dict[str, Any]] valid_color_configuration(bool setup_from_yaml)
Definition: schema_json.py:110
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
HomeAssistant async_get_hass()
Definition: core.py:286
str json_dumps(Any data)
Definition: json.py:149
JsonObjectType json_loads_object(bytes|bytearray|memoryview|str obj)
Definition: json.py:54