1 """Support for MQTT JSON lights."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from contextlib
import suppress
8 from typing
import TYPE_CHECKING, Any, cast
10 import voluptuous
as vol
25 DOMAIN
as LIGHT_DOMAIN,
35 filter_supported_color_modes,
36 valid_supported_color_modes,
59 from ..
import subscription
60 from ..config
import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA
66 DOMAIN
as MQTT_DOMAIN,
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,
76 MQTT_LIGHT_ATTRIBUTES_BLOCKED,
79 _LOGGER = logging.getLogger(__name__)
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"
93 DEFAULT_BRIGHTNESS_SCALE = 255
94 DEFAULT_WHITE_SCALE = 255
96 CONF_COLOR_MODE =
"color_mode"
97 CONF_SUPPORTED_COLOR_MODES =
"supported_color_modes"
99 CONF_EFFECT_LIST =
"effect_list"
101 CONF_FLASH_TIME_LONG =
"flash_time_long"
102 CONF_FLASH_TIME_SHORT =
"flash_time_short"
104 CONF_MAX_MIREDS =
"max_mireds"
105 CONF_MIN_MIREDS =
"min_mireds"
109 setup_from_yaml: bool,
110 ) -> Callable[[dict[str, Any]], dict[str, Any]]:
111 """Test color_mode is not combined with deprecated config."""
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:
119 "supported_color_modes must not "
120 f
"be combined with any of {deprecated}"
122 elif deprecated_flags_used:
123 deprecated_flags =
", ".join(key
for key
in deprecated
if key
in config)
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",
131 if not setup_from_yaml:
133 issue_id = hex(hash(frozenset(config)))
134 yaml_config_str = yaml_dump(config)
136 "https://www.home-assistant.io/integrations/"
137 f
"{LIGHT_DOMAIN}.mqtt/#json-schema"
144 issue_domain=LIGHT_DOMAIN,
146 severity=IssueSeverity.WARNING,
147 learn_more_url=learn_more_url,
148 translation_placeholders={
149 "deprecated_flags": deprecated_flags,
150 "config": yaml_config_str,
152 translation_key=
"deprecated_color_handling",
155 if CONF_COLOR_MODE
in config:
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",
162 if not setup_from_yaml:
164 issue_id = hex(hash(frozenset(config)))
165 yaml_config_str = yaml_dump(config)
167 "https://www.home-assistant.io/integrations/"
168 f
"{LIGHT_DOMAIN}.mqtt/#json-schema"
175 breaks_in_ha_version=
"2025.3.0",
176 issue_domain=LIGHT_DOMAIN,
178 severity=IssueSeverity.WARNING,
179 learn_more_url=learn_more_url,
180 translation_placeholders={
181 "config": yaml_config_str,
183 translation_key=
"deprecated_color_mode_flag",
188 return _valid_color_configuration
191 _PLATFORM_SCHEMA_BASE = (
192 MQTT_RW_SCHEMA.extend(
194 vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean,
196 CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE
197 ): vol.All(vol.Coerce(int), vol.Range(min=1)),
200 vol.Optional(CONF_COLOR_MODE): cv.boolean,
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]),
207 CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG
210 CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT
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])
221 vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
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(
228 [vol.In(VALID_COLOR_MODES)],
230 valid_supported_color_modes,
232 vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All(
233 vol.Coerce(int), vol.Range(min=1)
237 vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean,
240 .extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
241 .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema)
244 DISCOVERY_SCHEMA_JSON = vol.All(
246 _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
249 PLATFORM_SCHEMA_MODERN_JSON = vol.All(
251 _PLATFORM_SCHEMA_BASE,
256 """Representation of a MQTT JSON light."""
258 _default_name = DEFAULT_NAME
259 _entity_id_format = ENTITY_ID_FORMAT
260 _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
262 _fixed_color_mode: ColorMode | str |
None =
None
263 _flash_times: dict[str, int |
None]
264 _topic: dict[str, str |
None]
267 _deprecated_color_handling: bool =
False
271 """Return the config schema."""
272 return DISCOVERY_SCHEMA_JSON
275 """(Re)Setup the entity."""
281 key: config.get(key)
for key
in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC)
283 optimistic: bool = config[CONF_OPTIMISTIC]
289 for key
in (CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG)
293 LightEntityFeature.TRANSITION | LightEntityFeature.FLASH
296 config[CONF_EFFECT]
and LightEntityFeature.EFFECT
298 if supported_color_modes := self.
_config_config.
get(CONF_SUPPORTED_COLOR_MODES):
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)
321 red =
int(values[
"color"][
"r"])
322 green =
int(values[
"color"][
"g"])
323 blue =
int(values[
"color"][
"b"])
329 "Invalid RGB color value '%s' received for entity %s",
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)
343 "Invalid XY color value '%s' received for entity %s",
350 hue =
float(values[
"color"][
"h"])
351 saturation =
float(values[
"color"][
"s"])
357 "Invalid HS color value '%s' received for entity %s",
363 color_mode: str = values[
"color_mode"]
366 "Invalid color mode '%s' received for entity %s",
372 if color_mode == ColorMode.COLOR_TEMP:
375 elif color_mode == ColorMode.HS:
376 hue =
float(values[
"color"][
"h"])
377 saturation =
float(values[
"color"][
"s"])
380 elif color_mode == ColorMode.RGB:
381 r =
int(values[
"color"][
"r"])
382 g =
int(values[
"color"][
"g"])
383 b =
int(values[
"color"][
"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"])
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"])
401 elif color_mode == ColorMode.WHITE:
403 elif color_mode == ColorMode.XY:
404 x =
float(values[
"color"][
"x"])
405 y =
float(values[
"color"][
"y"])
408 except (KeyError, ValueError):
410 "Invalid or incomplete color value '%s' received for entity %s",
417 """Handle new MQTT messages."""
420 if values[
"state"] ==
"ON":
422 elif values[
"state"] ==
"OFF":
424 elif values[
"state"]
is None:
430 and "color" in values
433 if values[
"color"]
is None:
443 if brightness := values[
"brightness"]:
445 assert isinstance(brightness, float)
447 (1, self.
_config_config[CONF_BRIGHTNESS_SCALE]), brightness
451 "Ignoring zero brightness value for entity %s",
457 except (TypeError, ValueError):
459 "Invalid brightness value '%s' received for entity %s",
460 values[
"brightness"],
471 if values[
"color_temp"]
is None:
479 "Invalid color temp value '%s' received for entity %s",
480 values[
"color_temp"],
484 if "color" not in values:
488 with suppress(KeyError):
493 """(Re)Subscribe to topics."""
512 """(Re)Subscribe to topics."""
513 subscription.async_subscribe_topics_internal(self.
hasshasshass, self.
_sub_state_sub_state)
517 self.
_attr_is_on_attr_is_on = last_state.state == STATE_ON
518 last_attributes = last_state.attributes
541 """Return current color mode."""
548 if self.
hs_colorhs_color
is not None:
550 return ColorMode.COLOR_TEMP
553 if ATTR_TRANSITION
in kwargs:
554 message[
"transition"] = kwargs[ATTR_TRANSITION]
556 if ATTR_FLASH
in kwargs:
557 flash: str = kwargs[ATTR_FLASH]
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]
564 def _scale_rgbxx(self, rgbxx: tuple[int, ...], kwargs: Any) -> tuple[int, ...]:
569 if self.
_config_config[CONF_BRIGHTNESS]:
572 brightness = kwargs.pop(ATTR_BRIGHTNESS, 255)
573 return tuple(round(i / 255 * brightness)
for i
in rgbxx)
576 """Return True if the light natively supports a color mode."""
584 """Turn the device on.
586 This method is a coroutine.
589 should_update =
False
590 hs_color: tuple[float, float]
591 message: dict[str, Any] = {
"state":
"ON"}
593 rgbw: tuple[int, ...]
594 rgbcw: tuple[int, ...]
595 xy_color: tuple[float, float]
597 if ATTR_HS_COLOR
in kwargs
and (
601 hs_color = kwargs[ATTR_HS_COLOR]
602 message[
"color"] = {}
603 if self.
_config_config[CONF_RGB]:
606 if self.
_config_config[CONF_BRIGHTNESS]:
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
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]
631 hs_color = kwargs[ATTR_HS_COLOR]
632 message[
"color"] = {
"h": hs_color[0],
"s": hs_color[1]}
639 rgb = self.
_scale_rgbxx_scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs)
640 message[
"color"] = {
"r": rgb[0],
"g": rgb[1],
"b": rgb[2]}
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]}
651 self.
_attr_rgbw_color_attr_rgbw_color = cast(tuple[int, int, int, int], rgbw)
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)
665 self.
_attr_rgbww_color_attr_rgbww_color = cast(tuple[int, int, int, int, int], rgbcw)
669 xy_color = kwargs[ATTR_XY_COLOR]
670 message[
"color"] = {
"x": xy_color[0],
"y": xy_color[1]}
681 device_brightness = color_util.brightness_to_value(
682 (1, self.
_config_config[CONF_BRIGHTNESS_SCALE]),
683 kwargs[ATTR_BRIGHTNESS],
686 device_brightness =
max(round(device_brightness), 1)
687 message[
"brightness"] = device_brightness
693 if ATTR_COLOR_TEMP
in kwargs:
694 message[
"color_temp"] =
int(kwargs[ATTR_COLOR_TEMP])
702 if ATTR_EFFECT
in kwargs:
703 message[
"effect"] = kwargs[ATTR_EFFECT]
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)
714 device_white_level =
max(device_white_level, 1)
715 message[
"white"] = device_white_level
735 """Turn the device off.
737 This method is a coroutine.
739 message: dict[str, Any] = {
"state":
"OFF"}
int|None brightness(self)
tuple[int, int, int]|None rgb_color(self)
tuple[int, int, int, int]|None rgbw_color(self)
tuple[int, int, int, int, int]|None rgbww_color(self)
int|None color_temp(self)
set[ColorMode]|set[str]|None supported_color_modes(self)
tuple[float, float]|None hs_color(self)
ColorMode|str|None color_mode(self)
tuple[float, float]|None xy_color(self)
LightEntityFeature supported_features(self)
None async_publish_with_config(self, str topic, PublishPayloadType payload)
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
None _update_color(self, dict[str, Any] values)
None _state_received(self, ReceiveMessage msg)
VolSchemaType config_schema()
_attr_supported_color_modes
ColorMode|str|None color_mode(self)
None _setup_from_config(self, ConfigType config)
None async_turn_on(self, **Any kwargs)
None async_turn_off(self, **Any kwargs)
None _subscribe_topics(self)
tuple[int,...] _scale_rgbxx(self, tuple[int,...] rgbxx, Any kwargs)
_deprecated_color_handling
None _set_flash_and_transition(self, dict[str, Any] message, **Any kwargs)
None _prepare_subscribe_topics(self)
bool _supports_color_mode(self, ColorMode|str color_mode)
None async_write_ha_state(self)
int|None supported_features(self)
State|None async_get_last_state(self)
web.Response get(self, web.Request request, str config_key)
bool color_supported(Iterable[ColorMode|str]|None color_modes)
set[ColorMode] filter_supported_color_modes(Iterable[ColorMode] color_modes)
bool brightness_supported(Iterable[ColorMode|str]|None color_modes)
Callable[[dict[str, Any]], dict[str, Any]] valid_color_configuration(bool setup_from_yaml)
None async_create_issue(HomeAssistant hass, str entry_id)
HomeAssistant async_get_hass()
JsonObjectType json_loads_object(bytes|bytearray|memoryview|str obj)