From 67f3e717a8b5e3da5954da70e6fe91c0919ab876 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Jun 2021 17:43:04 +0200 Subject: [PATCH] Add support for color_mode white to tasmota light (#51608) --- homeassistant/components/tasmota/light.py | 65 +++++++---------- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/light/common.py | 5 ++ tests/components/tasmota/test_light.py | 71 +++++++++++++------ 6 files changed, 80 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 58a1ff1fb239b7..edbb01eefe6e3b 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -13,13 +13,13 @@ ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, - COLOR_MODE_RGBW, + COLOR_MODE_WHITE, SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, @@ -60,6 +60,17 @@ def clamp(value): return min(max(value, 0), 255) +def scale_brightness(brightness): + """Scale brightness from 0..255 to 1..100.""" + brightness_normalized = brightness / DEFAULT_BRIGHTNESS_MAX + device_brightness = min( + round(brightness_normalized * TASMOTA_BRIGHTNESS_MAX), + TASMOTA_BRIGHTNESS_MAX, + ) + # Make sure the brightness is not rounded down to 0 + return max(device_brightness, 1) + + class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, @@ -80,7 +91,6 @@ def __init__(self, **kwds): self._white_value = None self._flash_times = None self._rgb = None - self._rgbw = None super().__init__( **kwds, @@ -107,8 +117,7 @@ def _setup_from_entity(self): 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 + self._supported_color_modes.add(COLOR_MODE_WHITE) if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]: self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -150,7 +159,13 @@ def state_updated(self, state, **kwargs): white_value = float(attributes["white_value"]) percent_white = white_value / TASMOTA_BRIGHTNESS_MAX self._white_value = percent_white * 255 - if self._tasmota_entity.light_type == LIGHT_TYPE_RGBCW: + if self._tasmota_entity.light_type == LIGHT_TYPE_RGBW: + # Tasmota does not support RGBW mode, set mode to white or rgb + if self._white_value == 0: + self._color_mode = COLOR_MODE_RGB + else: + self._color_mode = COLOR_MODE_WHITE + elif 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 @@ -211,30 +226,9 @@ def rgb_color(self): blue_compensated = 0 return [red_compensated, green_compensated, blue_compensated] - @property - def rgbw_color(self): - """Return the rgbw color value.""" - if self._rgb is None or self._white_value is None: - return None - rgb = self._rgb - # Tasmota's color is adjusted for brightness, compensate - if self._brightness > 0: - 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)) - white_compensated = clamp(round(self._white_value / self._brightness * 255)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - white_compensated = 0 - return [red_compensated, green_compensated, blue_compensated, white_compensated] - @property def force_update(self): """Force update.""" - if self.color_mode == COLOR_MODE_RGBW: - return True return False @property @@ -262,25 +256,14 @@ async def async_turn_on(self, **kwargs): 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] - attributes["color"] = [rgbw[0], rgbw[1], rgbw[2], rgbw[3]] - # Tasmota does not support direct RGBW control, the light must be set to - # either white mode or color mode. Set the mode to white if white channel - # is on, and to color otherwise + if ATTR_WHITE in kwargs and COLOR_MODE_WHITE in supported_color_modes: + attributes["white_value"] = scale_brightness(kwargs[ATTR_WHITE]) if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] 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), - TASMOTA_BRIGHTNESS_MAX, - ) - # Make sure the brightness is not rounded down to 0 - device_brightness = max(device_brightness, 1) - attributes["brightness"] = device_brightness + attributes["brightness"] = scale_brightness(kwargs[ATTR_BRIGHTNESS]) if ATTR_COLOR_TEMP in kwargs and COLOR_MODE_COLOR_TEMP in supported_color_modes: attributes["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index cb869b6099c448..1c73e99e916573 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.14"], + "requirements": ["hatasmota==0.2.15"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 820545466c04a2..87ca3323c73089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.14 +hatasmota==0.2.15 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c3d5c82c31d44..75aa56d77ce9d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ hangups==0.4.14 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.14 +hatasmota==0.2.15 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 229823ceb174f9..0c16e0f2703b43 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -17,6 +17,7 @@ ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, @@ -50,6 +51,7 @@ def turn_on( flash=None, effect=None, color_name=None, + white=None, ): """Turn all or specified light on.""" hass.add_job( @@ -71,6 +73,7 @@ def turn_on( flash, effect, color_name, + white, ) @@ -92,6 +95,7 @@ async def async_turn_on( flash=None, effect=None, color_name=None, + white=None, ): """Turn all or specified light on.""" data = { @@ -113,6 +117,7 @@ async def async_turn_on( (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), + (ATTR_WHITE, white), ] if value is not None } diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index b74799d1d122bd..0c0e0a4e566836 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -223,8 +223,8 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): state.attributes.get("supported_features") == SUPPORT_EFFECT | SUPPORT_TRANSITION ) - assert state.attributes.get("supported_color_modes") == ["rgb", "rgbw"] - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("supported_color_modes") == ["rgb", "white"] + assert state.attributes.get("color_mode") == "rgb" async def test_attributes_rgbww(hass, mqtt_mock, setup_tasmota): @@ -434,7 +434,7 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): 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") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') state = hass.states.get("light.test") @@ -442,24 +442,31 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): assert "color_mode" not in state.attributes async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":0}' ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":75,"White":75}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 191.25 + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", - '{"POWER":"ON","Color":"128,64,0","White":0}', + '{"POWER":"ON","Dimmer":50,"Color":"128,64,0","White":0}', ) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 127.5 assert state.attributes.get("rgb_color") == (255, 128, 0) - assert state.attributes.get("rgbw_color") == (255, 128, 0, 0) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' @@ -467,9 +474,8 @@ async def test_controlling_state_via_mqtt_rgbw(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("rgb_color") == (255, 192, 128) - assert state.attributes.get("rgbw_color") == (255, 128, 0, 255) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' @@ -477,9 +483,8 @@ async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 0 - assert state.attributes.get("rgb_color") == (0, 0, 0) - assert state.attributes.get("rgbw_color") == (0, 0, 0, 0) - assert state.attributes.get("color_mode") == "rgbw" + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("color_mode") == "white" async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' @@ -959,21 +964,31 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - # Set color when setting white is off + # Set white when setting white + await common.async_turn_on(hass, "light.test", white=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() + + # rgbw_color should be ignored await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + "NoDelay;Power1 ON", 0, False, ) mqtt_mock.async_publish.reset_mock() - # Set white when white is on + # rgbw_color should be ignored 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", + "NoDelay;Power1 ON", 0, False, ) @@ -1041,7 +1056,7 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): # 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;Dimmer4 75", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -1055,21 +1070,31 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - # Set color when setting white is off + # Set white when setting white + await common.async_turn_on(hass, "light.test", white=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() + + # rgbw_color should be ignored await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON;NoDelay;Color2 128,64,32,0", + "NoDelay;Power1 ON", 0, False, ) mqtt_mock.async_publish.reset_mock() - # Set white when white is on + # rgbw_color should be ignored 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;Color2 16,64,32,128", + "NoDelay;Power1 ON", 0, False, )