diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index efab8dcaae3..440b6f4267d 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -12,20 +12,21 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, SUPPORT_EFFECT, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, + brightness_supported, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util from .const import DATA_REMOVE_DISCOVER_COMPONENT from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW @@ -64,14 +65,17 @@ class TasmotaLight( def __init__(self, **kwds): """Initialize Tasmota light.""" self._state = False + self._supported_color_modes = None self._supported_features = 0 self._brightness = None + self._color_mode = None self._color_temp = None self._effect = None - self._hs = None self._white_value = None self._flash_times = None + self._rgb = None + self._rgbw = None super().__init__( **kwds, @@ -87,22 +91,35 @@ class TasmotaLight( def _setup_from_entity(self): """(Re)Setup the entity.""" + self._supported_color_modes = set() supported_features = 0 light_type = self._tasmota_entity.light_type - if light_type != LIGHT_TYPE_NONE: - supported_features |= SUPPORT_BRIGHTNESS + if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: + # Mark RGB support for RGBW light because we don't have control over the + # white channel, so the base component's RGB->RGBW translation does not work + self._supported_color_modes.add(COLOR_MODE_RGB) + self._color_mode = COLOR_MODE_RGB + + if light_type == LIGHT_TYPE_RGBW: + self._supported_color_modes.add(COLOR_MODE_RGBW) + self._color_mode = COLOR_MODE_RGBW if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_COLOR_TEMP + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_mode = COLOR_MODE_COLOR_TEMP + + if light_type != LIGHT_TYPE_NONE and not self._supported_color_modes: + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + self._color_mode = COLOR_MODE_BRIGHTNESS + + if not self._supported_color_modes: + self._supported_color_modes.add(COLOR_MODE_ONOFF) + self._color_mode = COLOR_MODE_ONOFF if light_type in [LIGHT_TYPE_RGB, LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_COLOR supported_features |= SUPPORT_EFFECT - if light_type in [LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]: - supported_features |= SUPPORT_WHITE_VALUE - if self._tasmota_entity.supports_transition: supported_features |= SUPPORT_TRANSITION @@ -119,8 +136,17 @@ class TasmotaLight( percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 if "color" in attributes: - color = attributes["color"] - self._hs = color_util.color_RGB_to_hs(*color) + + def clamp(value): + """Clamp value to the range 0..255.""" + return min(max(value, 0), 255) + + rgb = attributes["color"] + # Tasmota's RGB color is adjusted for brightness, compensate + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + self._rgb = [red_compensated, green_compensated, blue_compensated] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -129,11 +155,13 @@ class TasmotaLight( white_value = float(attributes["white_value"]) percent_white = white_value / TASMOTA_BRIGHTNESS_MAX self._white_value = percent_white * 255 - if self._white_value == 0: - self._color_temp = None - self._white_value = None - if self._white_value is not None and self._white_value > 0: - self._hs = None + if self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: + # Tasmota does not support RGBWW mode, set mode to ct or rgb + if self._white_value == 0: + self._color_mode = COLOR_MODE_RGB + else: + self._color_mode = COLOR_MODE_COLOR_TEMP + self.async_write_ha_state() @property @@ -141,6 +169,11 @@ class TasmotaLight( """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_mode(self): + """Return the color mode of the light.""" + return self._color_mode + @property def color_temp(self): """Return the color temperature in mired.""" @@ -167,20 +200,27 @@ class TasmotaLight( return self._tasmota_entity.effect_list @property - def hs_color(self): - """Return the hs color value.""" - return self._hs + def rgb_color(self): + """Return the rgb color value.""" + return self._rgb @property - def white_value(self): - """Return the white property.""" - return self._white_value + def rgbw_color(self): + """Return the rgbw color value.""" + if self._rgb is None or self._white_value is None: + return None + return [*self._rgb, self._white_value] @property def is_on(self): """Return true if device is on.""" return self._state + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" @@ -188,21 +228,33 @@ class TasmotaLight( async def async_turn_on(self, **kwargs): """Turn the entity on.""" - supported_features = self._supported_features + supported_color_modes = self._supported_color_modes attributes = {} - if ATTR_HS_COLOR in kwargs and supported_features & SUPPORT_COLOR: - hs_color = kwargs[ATTR_HS_COLOR] - attributes["color"] = {} - - rgb = color_util.color_hsv_to_RGB(hs_color[0], hs_color[1], 100) + if ATTR_RGB_COLOR in kwargs and COLOR_MODE_RGB in supported_color_modes: + rgb = kwargs[ATTR_RGB_COLOR] attributes["color"] = [rgb[0], rgb[1], rgb[2]] + if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: + rgbw = kwargs[ATTR_RGBW_COLOR] + # Tasmota does not support direct RGBW control, the light must be set to + # either white mode or color mode. Set the mode according to max of rgb + # and white channels + if max(rgbw[0:3]) > rgbw[3]: + attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] + else: + white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX + device_white_value = min( + round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), + TASMOTA_BRIGHTNESS_MAX, + ) + attributes["white_value"] = device_white_value + if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] - if ATTR_BRIGHTNESS in kwargs and supported_features & SUPPORT_BRIGHTNESS: + if ATTR_BRIGHTNESS in kwargs and brightness_supported(supported_color_modes): brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_MAX device_brightness = min( round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), @@ -212,20 +264,12 @@ class TasmotaLight( device_brightness = max(device_brightness, 1) attributes["brightness"] = device_brightness - if ATTR_COLOR_TEMP in kwargs and supported_features & SUPPORT_COLOR_TEMP: + if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if ATTR_EFFECT in kwargs: attributes["effect"] = kwargs[ATTR_EFFECT] - if ATTR_WHITE_VALUE in kwargs: - white_value_normalized = kwargs[ATTR_WHITE_VALUE] / DEFAULT_BRIGHTNESS_MAX - device_white_value = min( - round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), - TASMOTA_BRIGHTNESS_MAX, - ) - attributes["white_value"] = device_white_value - self._tasmota_entity.set_state(True, attributes) async def async_turn_off(self, **kwargs): diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index a60f167c38f..6b450fa805d 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -11,14 +11,7 @@ from hatasmota.utils import ( ) from homeassistant.components import light -from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_EFFECT, - SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, -) +from homeassistant.components.light import SUPPORT_EFFECT, SUPPORT_TRANSITION from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON @@ -60,6 +53,8 @@ async def test_attributes_on_off(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None assert state.attributes.get("supported_features") == 0 + assert state.attributes.get("supported_color_modes") == ["onoff"] + assert state.attributes.get("color_mode") == "onoff" async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): @@ -83,7 +78,9 @@ async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None - assert state.attributes.get("supported_features") == SUPPORT_BRIGHTNESS + assert state.attributes.get("supported_features") == 0 + assert state.attributes.get("supported_color_modes") == ["brightness"] + assert state.attributes.get("color_mode") == "brightness" async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): @@ -106,10 +103,9 @@ async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") is None assert state.attributes.get("max_mireds") is None - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["brightness"] + assert state.attributes.get("color_mode") == "brightness" async def test_attributes_ct(hass, mqtt_mock, setup_tasmota): @@ -132,10 +128,9 @@ async def test_attributes_ct(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 500 - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["color_temp"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_ct_reduced(hass, mqtt_mock, setup_tasmota): @@ -159,10 +154,9 @@ async def test_attributes_ct_reduced(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("effect_list") is None assert state.attributes.get("min_mireds") == 200 assert state.attributes.get("max_mireds") == 380 - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["color_temp"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): @@ -193,8 +187,10 @@ async def test_attributes_rgb(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") is None assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_TRANSITION + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb"] + assert state.attributes.get("color_mode") == "rgb" async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): @@ -225,12 +221,10 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") is None assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb", "rgbw"] + assert state.attributes.get("color_mode") == "rgbw" async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): @@ -261,13 +255,10 @@ async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") == 500 assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("color_mode") == "color_temp" async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): @@ -299,13 +290,10 @@ async def test_attributes_rgbww_reduced(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("max_mireds") == 380 assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS - | SUPPORT_COLOR - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["color_temp", "rgb"] + assert state.attributes.get("color_mode") == "color_temp" async def test_controlling_state_via_mqtt_on_off(hass, mqtt_mock, setup_tasmota): @@ -325,29 +313,35 @@ async def test_controlling_state_via_mqtt_on_off(hass, mqtt_mock, setup_tasmota) state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "onoff" async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): @@ -367,19 +361,23 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -387,6 +385,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -394,6 +393,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" # Tasmota will send "Color" also for CT light, this should be ignored async_fire_mqtt_message( @@ -403,6 +403,7 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): @@ -422,19 +423,23 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -442,22 +447,27 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -465,15 +475,18 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - # Setting white to 0 should clear the white_value and color_temp - assert not state.attributes.get("white_value") - assert not state.attributes.get("color_temp") + # Setting white to 0 should clear the color_temp + assert "white_value" not in state.attributes + assert "color_temp" not in state.attributes + assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -511,19 +524,23 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -531,29 +548,33 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"804000","White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"0080400000"}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (0, 255, 128) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -561,6 +582,7 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' @@ -570,6 +592,7 @@ async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmo # Setting white to 0 should clear the white_value and color_temp assert not state.attributes.get("white_value") assert not state.attributes.get("color_temp") + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -607,19 +630,23 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("light.test") assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' @@ -627,22 +654,27 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 127.5 + assert "white_value" not in state.attributes # Setting white > 0 should clear the color - assert not state.attributes.get("rgb_color") + assert "rgb_color" not in state.attributes + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}' @@ -650,6 +682,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("color_temp") == 300 + assert state.attributes.get("color_mode") == "color_temp" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}' @@ -659,6 +692,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm # Setting white to 0 should clear the white_value and color_temp assert not state.attributes.get("white_value") assert not state.attributes.get("color_temp") + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -765,6 +799,102 @@ async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): ) +async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting color + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting brighter color than white + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 16]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set white when setting brighter white than color + await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;White 50", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", effect="Random") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Scheme 4", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -811,10 +941,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 255,128,0", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", 0, False, ) @@ -830,9 +960,10 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;White 50", + "NoDelay;Power1 ON", 0, False, ) @@ -1000,7 +1131,7 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Dimmer":50, "Color":"0,255,0"}', + '{"POWER":"ON","Dimmer":50, "Color":"0,255,0", "White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1040,7 +1171,9 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): # Fake state update from the light async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":50, "CT":153, "White":50}', ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1324,10 +1457,8 @@ async def test_discovery_update_reconfigure_light( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data1) await hass.async_block_till_done() state = hass.states.get("light.test") - assert ( - state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - ) + assert state.attributes.get("supported_features") == SUPPORT_TRANSITION + assert state.attributes.get("supported_color_modes") == ["brightness"] # Reconfigure as RGB light async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", data2) @@ -1335,8 +1466,9 @@ async def test_discovery_update_reconfigure_light( state = hass.states.get("light.test") assert ( state.attributes.get("supported_features") - == SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_TRANSITION + == SUPPORT_EFFECT | SUPPORT_TRANSITION ) + assert state.attributes.get("supported_color_modes") == ["rgb"] async def test_availability_when_connection_lost(