From 870c61a62262af07534fa41f70f651f3a680c5fd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 May 2021 11:37:02 +0200 Subject: [PATCH] Add color_mode support to MQTT light with basic schema (#50464) * Add color_mode support to MQTT light with basic schema * Update abbreviations * Silence pylint * Improve test coverage * Apply suggestions from code review --- .../components/mqtt/abbreviations.py | 10 + .../components/mqtt/light/schema_basic.py | 345 ++++- tests/components/mqtt/test_light.py | 1303 ++++++++++++++++- 3 files changed, 1613 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 23c94ada4c03a6..a16d721ba7c599 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -25,6 +25,8 @@ "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clrm": "color_mode", + "clrm_stat_t": "color_mode_state_topic", + "clrm_val_tpl": "color_mode_value_template", "clr_temp_cmd_t": "color_temp_command_topic", "clr_temp_stat_t": "color_temp_state_topic", "clr_temp_tpl": "color_temp_template", @@ -146,6 +148,14 @@ "rgb_cmd_t": "rgb_command_topic", "rgb_stat_t": "rgb_state_topic", "rgb_val_tpl": "rgb_value_template", + "rgbw_cmd_tpl": "rgbw_command_template", + "rgbw_cmd_t": "rgbw_command_topic", + "rgbw_stat_t": "rgbw_state_topic", + "rgbw_val_tpl": "rgbw_value_template", + "rgbww_cmd_tpl": "rgbww_command_template", + "rgbww_cmd_t": "rgbww_command_topic", + "rgbww_stat_t": "rgbww_state_topic", + "rgbww_val_tpl": "rgbww_value_template", "send_cmd_t": "send_command_topic", "send_if_off": "send_if_off", "set_fan_spd_t": "set_fan_speed_topic", diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index f7c1648e96b4d8..3e347363428ba0 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -5,13 +5,24 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_XY, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -44,6 +55,8 @@ CONF_BRIGHTNESS_SCALE = "brightness_scale" CONF_BRIGHTNESS_STATE_TOPIC = "brightness_state_topic" CONF_BRIGHTNESS_VALUE_TEMPLATE = "brightness_value_template" +CONF_COLOR_MODE_STATE_TOPIC = "color_mode_state_topic" +CONF_COLOR_MODE_VALUE_TEMPLATE = "color_mode_value_template" CONF_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template" CONF_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic" CONF_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic" @@ -61,6 +74,14 @@ CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" CONF_RGB_STATE_TOPIC = "rgb_state_topic" CONF_RGB_VALUE_TEMPLATE = "rgb_value_template" +CONF_RGBW_COMMAND_TEMPLATE = "rgbw_command_template" +CONF_RGBW_COMMAND_TOPIC = "rgbw_command_topic" +CONF_RGBW_STATE_TOPIC = "rgbw_state_topic" +CONF_RGBW_VALUE_TEMPLATE = "rgbw_value_template" +CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" +CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" +CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" +CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" @@ -81,13 +102,21 @@ VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] -COMMAND_TEMPLATE_KEYS = [CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE] +COMMAND_TEMPLATE_KEYS = [ + CONF_COLOR_TEMP_COMMAND_TEMPLATE, + CONF_RGB_COMMAND_TEMPLATE, + CONF_RGBW_COMMAND_TEMPLATE, + CONF_RGBWW_COMMAND_TEMPLATE, +] VALUE_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_VALUE_TEMPLATE, + CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_VALUE_TEMPLATE, CONF_EFFECT_VALUE_TEMPLATE, CONF_HS_VALUE_TEMPLATE, CONF_RGB_VALUE_TEMPLATE, + CONF_RGBW_VALUE_TEMPLATE, + CONF_RGBWW_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE, CONF_WHITE_VALUE_TEMPLATE, CONF_XY_VALUE_TEMPLATE, @@ -102,6 +131,8 @@ ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -126,6 +157,14 @@ vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -159,22 +198,32 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" self._brightness = None + self._color_mode = None self._color_temp = None self._effect = None self._hs_color = None + self._legacy_mode = False + self._rgb_color = None + self._rgbw_color = None + self._rgbww_color = None self._state = False + self._supported_color_modes = None self._white_value = None + self._xy_color = None self._topic = None self._payload = None self._command_templates = None self._value_templates = None self._optimistic = False - self._optimistic_rgb_color = False self._optimistic_brightness = False + self._optimistic_color_mode = False self._optimistic_color_temp = False self._optimistic_effect = False self._optimistic_hs_color = False + self._optimistic_rgb_color = False + self._optimistic_rgbw_color = False + self._optimistic_rgbww_color = False self._optimistic_white_value = False self._optimistic_xy_color = False @@ -192,6 +241,7 @@ def _setup_from_config(self, config): for key in ( CONF_BRIGHTNESS_COMMAND_TOPIC, CONF_BRIGHTNESS_STATE_TOPIC, + CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_TEMP_COMMAND_TOPIC, CONF_COLOR_TEMP_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -201,6 +251,10 @@ def _setup_from_config(self, config): CONF_HS_STATE_TOPIC, CONF_RGB_COMMAND_TOPIC, CONF_RGB_STATE_TOPIC, + CONF_RGBW_COMMAND_TOPIC, + CONF_RGBW_STATE_TOPIC, + CONF_RGBWW_COMMAND_TOPIC, + CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_WHITE_VALUE_STATE_TOPIC, @@ -230,8 +284,15 @@ def _setup_from_config(self, config): self._command_templates = command_templates optimistic = config[CONF_OPTIMISTIC] + self._optimistic_color_mode = ( + optimistic or topic[CONF_COLOR_MODE_STATE_TOPIC] is None + ) self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._optimistic_rgb_color = optimistic or topic[CONF_RGB_STATE_TOPIC] is None + self._optimistic_rgbw_color = optimistic or topic[CONF_RGBW_STATE_TOPIC] is None + self._optimistic_rgbww_color = ( + optimistic or topic[CONF_RGBWW_STATE_TOPIC] is None + ) self._optimistic_brightness = ( optimistic or ( @@ -252,6 +313,38 @@ def _setup_from_config(self, config): optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None ) self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None + self._supported_color_modes = set() + if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_mode = COLOR_MODE_COLOR_TEMP + if topic[CONF_HS_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_HS) + self._color_mode = COLOR_MODE_HS + if topic[CONF_RGB_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGB) + self._color_mode = COLOR_MODE_RGB + if topic[CONF_RGBW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBW) + self._color_mode = COLOR_MODE_RGBW + if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_RGBWW) + self._color_mode = COLOR_MODE_RGBWW + if topic[CONF_XY_COMMAND_TOPIC] is not None: + self._supported_color_modes.add(COLOR_MODE_XY) + self._color_mode = COLOR_MODE_XY + if len(self._supported_color_modes) > 1: + self._color_mode = COLOR_MODE_UNKNOWN + + if not self._supported_color_modes: + if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + self._color_mode = COLOR_MODE_BRIGHTNESS + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + else: + self._color_mode = COLOR_MODE_ONOFF + self._supported_color_modes.add(COLOR_MODE_ONOFF) + + if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: + self._legacy_mode = True def _is_optimistic(self, attribute): """Return True if the attribute is optimistically updated.""" @@ -334,6 +427,8 @@ def _rgbx_received(msg, template, color_mode, convert_color): ) return None color = tuple(int(val) for val in payload.split(",")) + if self._optimistic_color_mode: + self._color_mode = color_mode if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: rgb = convert_color(*color) percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 @@ -349,12 +444,69 @@ def rgb_received(msg): ) if not rgb: return - self._hs_color = color_util.color_RGB_to_hs(*rgb) + if self._legacy_mode: + self._hs_color = color_util.color_RGB_to_hs(*rgb) + else: + self._rgb_color = rgb self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) + restore_state(ATTR_RGB_COLOR) restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) + @callback + @log_messages(self.hass, self.entity_id) + def rgbw_received(msg): + """Handle new MQTT messages for RGBW.""" + rgbw = _rgbx_received( + msg, + CONF_RGBW_VALUE_TEMPLATE, + COLOR_MODE_RGBW, + color_util.color_rgbw_to_rgb, + ) + if not rgbw: + return + self._rgbw_color = rgbw + self.async_write_ha_state() + + add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) + restore_state(ATTR_RGBW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def rgbww_received(msg): + """Handle new MQTT messages for RGBWW.""" + rgbww = _rgbx_received( + msg, + CONF_RGBWW_VALUE_TEMPLATE, + COLOR_MODE_RGBWW, + color_util.color_rgbww_to_rgb, + ) + if not rgbww: + return + self._rgbww_color = rgbww + self.async_write_ha_state() + + add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) + restore_state(ATTR_RGBWW_COLOR) + + @callback + @log_messages(self.hass, self.entity_id) + def color_mode_received(msg): + """Handle new MQTT messages for color mode.""" + payload = self._value_templates[CONF_COLOR_MODE_VALUE_TEMPLATE]( + msg.payload, None + ) + if not payload: + _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) + return + + self._color_mode = payload + self.async_write_ha_state() + + add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) + restore_state(ATTR_COLOR_MODE) + @callback @log_messages(self.hass, self.entity_id) def color_temp_received(msg): @@ -366,6 +518,8 @@ def color_temp_received(msg): _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_COLOR_TEMP self._color_temp = int(payload) self.async_write_ha_state() @@ -399,6 +553,8 @@ def hs_received(msg): return try: hs_color = tuple(float(val) for val in payload.split(",", 2)) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_HS self._hs_color = hs_color self.async_write_ha_state() except ValueError: @@ -436,10 +592,16 @@ def xy_received(msg): return xy_color = tuple(float(val) for val in payload.split(",")) - self._hs_color = color_util.color_xy_to_hs(*xy_color) + if self._optimistic_color_mode: + self._color_mode = COLOR_MODE_XY + if self._legacy_mode: + self._hs_color = color_util.color_xy_to_hs(*xy_color) + else: + self._xy_color = xy_color self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) + restore_state(ATTR_XY_COLOR) restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) self._sub_state = await subscription.async_subscribe_topics( @@ -454,16 +616,51 @@ def brightness(self): brightness = min(round(brightness), 255) return brightness + @property + def color_mode(self): + """Return current color mode.""" + if self._legacy_mode: + return None + return self._color_mode + @property def hs_color(self): """Return the hs color value.""" + if not self._legacy_mode: + return self._hs_color + + # Legacy mode, gate color_temp with white_value == 0 if self._white_value: return None return self._hs_color + @property + def rgb_color(self): + """Return the rgb color value.""" + return self._rgb_color + + @property + def rgbw_color(self): + """Return the rgbw color value.""" + return self._rgbw_color + + @property + def rgbww_color(self): + """Return the rgbww color value.""" + return self._rgbww_color + + @property + def xy_color(self): + """Return the xy color value.""" + return self._xy_color + @property def color_temp(self): """Return the color temperature in mired.""" + if not self._legacy_mode: + return self._color_temp + + # Legacy mode, gate color_temp with white_value > 0 supports_color = ( self._topic[CONF_RGB_COMMAND_TOPIC] or self._topic[CONF_HS_COMMAND_TOPIC] @@ -512,10 +709,24 @@ def effect(self): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self): + """Flag supported color modes.""" + if self._legacy_mode: + return None + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" supported_features = 0 + supported_features |= ( + self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT + ) + if not self._legacy_mode: + return supported_features + + # Legacy mode supported_features |= self._topic[CONF_RGB_COMMAND_TOPIC] is not None and ( SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) @@ -527,9 +738,6 @@ def supported_features(self): self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP ) - supported_features |= ( - self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and SUPPORT_EFFECT - ) supported_features |= ( self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR ) @@ -543,7 +751,7 @@ def supported_features(self): return supported_features - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. @@ -574,22 +782,28 @@ def scale_rgbx(color, brightness=None): ) return tuple(int(channel * brightness / 255) for channel in color) - def render_rgbx(color, template): + def render_rgbx(color, template, color_mode): """Render RGBx payload.""" tpl = self._command_templates[template] if tpl: keys = ["red", "green", "blue"] + if color_mode == COLOR_MODE_RGBW: + keys.append("white") + elif color_mode == COLOR_MODE_RGBWW: + keys.extend(["cold_white", "warm_white"]) rgb_color_str = tpl(zip(keys, color)) else: rgb_color_str = ",".join(str(channel) for channel in color) return rgb_color_str - def set_optimistic(attribute, value, condition_attribute=None): + def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): """Optimistically update a state attribute.""" if condition_attribute is None: condition_attribute = attribute if not self._is_optimistic(condition_attribute): return False + if color_mode and self._optimistic_color_mode: + self._color_mode = color_mode setattr(self, f"_{attribute}", value) return True @@ -604,21 +818,72 @@ def set_optimistic(attribute, value, condition_attribute=None): kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 hs_color = kwargs.get(ATTR_HS_COLOR) - if hs_color and self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - # Convert HS to RGB + if ( + hs_color + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to RGB rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_RGB_COLOR) + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR + ) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color) + should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, COLOR_MODE_HS) - if hs_color and self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if ( + hs_color + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and self._legacy_mode + ): + # Legacy mode: Convert HS to XY xy_color = color_util.color_hs_to_xy(*hs_color) publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") - should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ATTR_XY_COLOR) + should_update |= set_optimistic( + ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR + ) + + if ( + (rgb := kwargs.get(ATTR_RGB_COLOR)) + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgb) + rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, COLOR_MODE_RGB) + + if ( + (rgbw := kwargs.get(ATTR_RGBW_COLOR)) + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbw) + rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, COLOR_MODE_RGBW) + + if ( + (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + scaled = scale_rgbx(rgbww) + rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, COLOR_MODE_RGBWW) + + if ( + (xy_color := kwargs.get(ATTR_XY_COLOR)) + and self._topic[CONF_XY_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, COLOR_MODE_XY) if ( ATTR_BRIGHTNESS in kwargs @@ -637,14 +902,52 @@ def set_optimistic(attribute, value, condition_attribute=None): ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and self._legacy_mode ): + # Legacy mode hs_color = self._hs_color if self._hs_color is not None else (0, 0) brightness = kwargs[ATTR_BRIGHTNESS] rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) - + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGB_COLOR not in kwargs + and self._topic[CONF_RGB_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 + rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) + rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, COLOR_MODE_RGB) + publish(CONF_RGB_COMMAND_TOPIC, rgb_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBW_COLOR not in kwargs + and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbw_color = ( + self._rgbw_color if self._rgbw_color is not None else (255,) * 4 + ) + rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) + rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, COLOR_MODE_RGBW) + publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) + elif ( + ATTR_BRIGHTNESS in kwargs + and ATTR_RGBWW_COLOR not in kwargs + and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None + and not self._legacy_mode + ): + rgbww_color = ( + self._rgbww_color if self._rgbww_color is not None else (255,) * 5 + ) + rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) + rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, COLOR_MODE_RGBWW) + publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) + should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) if ( ATTR_COLOR_TEMP in kwargs and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None @@ -655,7 +958,9 @@ def set_optimistic(attribute, value, condition_attribute=None): color_temp = tpl({"value": color_temp}) publish(CONF_COLOR_TEMP_COMMAND_TOPIC, color_temp) - should_update |= set_optimistic(ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP]) + should_update |= set_optimistic( + ATTR_COLOR_TEMP, kwargs[ATTR_COLOR_TEMP], COLOR_MODE_COLOR_TEMP + ) if ATTR_EFFECT in kwargs and self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: effect = kwargs[ATTR_EFFECT] diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e995b373d03706..e419743bd870ef 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -212,8 +212,8 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): - """Test RGB light flags brightness support.""" +async def test_legacy_rgb_white_light(hass, mqtt_mock): + """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, light.DOMAIN, @@ -223,14 +223,19 @@ async def test_rgb_light(hass, mqtt_mock): "name": "test", "command_topic": "test_light_rgb/set", "rgb_command_topic": "test_light_rgb/rgb/set", + "white_value_command_topic": "test_light_rgb/white/set", } }, ) await hass.async_block_till_done() state = hass.states.get("light.test") - expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS + expected_features = ( + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS | light.SUPPORT_WHITE_VALUE + ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): @@ -255,8 +260,13 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] async_fire_mqtt_message(hass, "test_light_rgb/status", "ON") @@ -266,12 +276,17 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] -async def test_controlling_state_via_topic(hass, mqtt_mock): - """Test the controlling of the state via topic.""" +async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { "platform": "mqtt", @@ -297,6 +312,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "payload_off": 0, } } + color_modes = ["color_temp", "hs", "rgbw"] assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() @@ -308,8 +324,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -321,8 +342,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "0") @@ -335,20 +361,28 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes["brightness"] == 100 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") light_state = hass.states.get("light.test") assert light_state.attributes.get("color_temp") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "100") light_state = hass.states.get("light.test") assert light_state.attributes["white_value"] == 100 assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") light_state = hass.states.get("light.test") assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -356,23 +390,152 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "0") light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (255, 255, 255) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") light_state = hass.states.get("light.test") assert light_state.attributes.get("xy_color") == (0.672, 0.324) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") == 100 + assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -488,6 +651,140 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 +async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): + """Test handling of empty data via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "255,255,255") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "255") + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") == (0, 0) + assert state.attributes.get("xy_color") == (0.323, 0.329) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message(hass, "test_light_rgb/status", "") + assert "Ignoring empty state message" in caplog.text + light_state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") + assert "Ignoring empty brightness message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["brightness"] == 255 + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "") + assert "Ignoring empty color mode message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") + assert "Ignoring empty effect message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") + assert "Ignoring empty rgb message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (255, 255, 255) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") + assert "Ignoring empty hs message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") + assert "Failed to parse hs state update" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") + assert "Ignoring empty xy-color message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.323, 0.329) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "255,255,255,1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "") + assert "Ignoring empty rgbw message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (255, 255, 255, 1) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "255,255,255,1,2") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "") + assert "Ignoring empty rgbww message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (255, 255, 255, 1, 2) + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "153") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") == 153 + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") + assert "Ignoring empty color temp message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["color_temp"] == 153 + + async def test_brightness_controlling_scale(hass, mqtt_mock): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): @@ -574,7 +871,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -621,7 +918,7 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -706,36 +1003,136 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get("xy_color") == (0.14, 0.131) -async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): - """Test the setting of the state with undocumented value_template.""" +async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): + """Test the setting of the state with a template.""" config = { light.DOMAIN: { "platform": "mqtt", "name": "test", "state_topic": "test_light_rgb/status", "command_topic": "test_light_rgb/set", - "value_template": "{{ value_json.hello }}", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbw/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "effect_state_topic": "test_light_rgb/effect/status", + "hs_state_topic": "test_light_rgb/hs/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "xy_state_topic": "test_light_rgb/xy/status", + "state_value_template": "{{ value_json.hello }}", + "brightness_value_template": "{{ value_json.hello }}", + "color_temp_value_template": "{{ value_json.hello }}", + "effect_value_template": "{{ value_json.hello }}", + "hs_value_template": '{{ value_json.hello | join(",") }}', + "rgb_value_template": '{{ value_json.hello | join(",") }}', + "rgbw_value_template": '{{ value_json.hello | join(",") }}', + "rgbww_value_template": '{{ value_json.hello | join(",") }}', + "xy_value_template": '{{ value_json.hello | join(",") }}', } } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("rgb_color") is None + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", '{"hello": [1, 2, 3]}') async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", '{"hello": "50"}') + async_fire_mqtt_message( + hass, "test_light_rgb/effect/status", '{"hello": "rainbow"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("rgb_color") == (1, 2, 3) + assert state.attributes.get("effect") == "rainbow" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + async_fire_mqtt_message( + hass, "test_light_rgb/rgbw/status", '{"hello": [1, 2, 3, 4]}' + ) state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("rgbw_color") == (1, 2, 3, 4) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "OFF"}') + async_fire_mqtt_message( + hass, "test_light_rgb/rgbww/status", '{"hello": [1, 2, 3, 4, 5]}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgbww_color") == (1, 2, 3, 4, 5) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + async_fire_mqtt_message( + hass, "test_light_rgb/color_temp/status", '{"hello": "300"}' + ) state = hass.states.get("light.test") - assert state.state == STATE_OFF + assert state.attributes.get("color_temp") == 300 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') + state = hass.states.get("light.test") + assert state.attributes.get("hs_color") == (100, 50) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + async_fire_mqtt_message( + hass, "test_light_rgb/xy/status", '{"hello": [0.123,0.123]}' + ) + state = hass.states.get("light.test") + assert state.attributes.get("xy_color") == (0.123, 0.123) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): + """Test the setting of the state with undocumented value_template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "value_template": "{{ value_json.hello }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + +async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -755,6 +1152,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "payload_off": "off", } } + color_modes = ["color_temp", "hs", "rgbw"] fake_state = ha.State( "light.test", "on", @@ -782,18 +1180,20 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("white_value") is None assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_on(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "on", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_off(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "off", 2, False ) @@ -805,8 +1205,19 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -829,6 +1240,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -848,6 +1262,195 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test the sending of command in optimistic mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "effect_list": ["colorloop", "random"], + "qos": 2, + "payload_on": "on", + "payload_off": "off", + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + fake_state = ha.State( + "light.test", + "on", + { + "brightness": 95, + "hs_color": [100, 100], + "effect": "random", + "color_temp": 100, + "color_mode": "hs", + }, + ) + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ), assert_setup_component(1, light.DOMAIN): + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 95 + assert state.attributes.get("hs_color") == (100, 100) + assert state.attributes.get("effect") == "random" + assert state.attributes.get("color_temp") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "light.test", effect="colorloop") + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/effect/set", "colorloop", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=10, rgb_color=[80, 40, 20] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "10", 2, False), + call("test_light_rgb/rgb/set", "80,40,20", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 10 + assert state.attributes.get("rgb_color") == (80, 40, 20) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=20, rgbw_color=[80, 40, 20, 10] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "20", 2, False), + call("test_light_rgb/rgbw/set", "80,40,20,10", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 20 + assert state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=40, rgbww_color=[80, 40, 20, 10, 8] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "40", 2, False), + call("test_light_rgb/rgbww/set", "80,40,20,10,8", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 40 + assert state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "50", 2, False), + call("test_light_rgb/hs/set", "359.0,78.0", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("hs_color") == (359.0, 78.0) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=60, xy_color=[0.2, 0.3]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "60", 2, False), + call("test_light_rgb/xy/set", "0.2,0.3", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("xy_color") == (0.2, 0.3) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", color_temp=125) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/color_temp/set", "125", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("color_temp") == 125 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): """Test the sending of RGB command with template.""" config = { @@ -875,14 +1478,88 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ call("test_light_rgb/set", "on", 0, False), - call("test_light_rgb/rgb/set", "#ff803f", 0, False), + call("test_light_rgb/rgb/set", "#ff8040", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgb_color"] == (255, 128, 64) + + +async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): + """Test the sending of RGBW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbw_command_template": '{{ "#%02x%02x%02x%02x" | ' + "format(red, green, blue, white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 64, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbw/set", "#ff804020", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgbw_color"] == (255, 128, 64, 32) + + +async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): + """Test the sending of RGBWW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "rgbww_command_template": '{{ "#%02x%02x%02x%02x%02x" | ' + "format(red, green, blue, cold_white, warm_white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 64, 32, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbww/set", "#ff80402010", 0, False), ], any_order=True, ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 63) + assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): @@ -1117,7 +1794,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) -async def test_on_command_rgb(hass, mqtt_mock): +async def test_legacy_on_command_rgb(hass, mqtt_mock): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1125,6 +1802,7 @@ async def test_on_command_rgb(hass, mqtt_mock): "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", + "white_value_command_topic": "test_light/white_value", } } @@ -1207,15 +1885,14 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb_template(hass, mqtt_mock): - """Test on command in RGB brightness mode with RGB template.""" +async def test_on_command_rgb(hass, mqtt_mock): + """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { "platform": "mqtt", "name": "test", "command_topic": "test_light/set", "rgb_command_topic": "test_light/rgb", - "rgb_command_template": "{{ red }}/{{ green }}/{{ blue }}", } } @@ -1232,16 +1909,592 @@ async def test_on_command_rgb_template(hass, mqtt_mock): # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ - call("test_light/rgb", "127/127/127", 0, False), + call("test_light/rgb", "127,127,127", 0, False), call("test_light/set", "ON", 0, False), ], any_order=True, ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_off(hass, "light.test") + await common.async_turn_on(hass, "light.test", brightness=255) - mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + # Should get the following MQTT messages. + # test_light/rgb: '255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgb: '1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_on_command_rgbw(hass, mqtt_mock): + """Test on command in RGBW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbw: '127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbw: '1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 0, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,128,0,16", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_on_command_rgbww(hass, mqtt_mock): + """Test on command in RGBWW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbww: '127,127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127,127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbww: '1,1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 0, 16, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,0,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,128,0,16,32' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,128,0,16,32", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_on_command_rgb_template(hass, mqtt_mock): + """Test on command in RGB brightness mode with RGB template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgb_command_topic": "test_light/rgb", + "rgb_command_template": "{{ red }}/{{ green }}/{{ blue }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_on_command_rgbw_template(hass, mqtt_mock): + """Test on command in RGBW brightness mode with RGBW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + "rgbw_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_on_command_rgbww_template(hass, mqtt_mock): + """Test on command in RGBWW brightness mode with RGBWW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + "rgbww_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ cold_white }}/{{ warm_white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127/127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_explicit_color_mode(hass, mqtt_mock): + """Test explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "hs") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "xy") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_explicit_color_mode_templated(hass, mqtt_mock): + """Test templated explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "color_mode_value_template": "{{ value_json.color_mode }}", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"color_temp"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"hs"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async def test_effect(hass, mqtt_mock):