From 9a8317db1de760c86415535334319cd990fd44b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery <erik@montnemery.com> Date: Sat, 15 May 2021 19:36:08 +0200 Subject: [PATCH] Bump hatasmota to 0.2.13 (#50662) * Bump hatasmota to 0.2.13 * Process review comment Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Tweak brightness compensation, improve tests Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --- homeassistant/components/tasmota/light.py | 62 +++--- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_common.py | 2 +- tests/components/tasmota/test_light.py | 206 +++++++++++++++++- 6 files changed, 241 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 53db34a9001..58a1ff1fb23 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -55,6 +55,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def clamp(value): + """Clamp value to the range 0..255.""" + return min(max(value, 0), 255) + + class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, @@ -136,22 +141,7 @@ class TasmotaLight( percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 if "color" in attributes: - - 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 - 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)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - self._rgb = [red_compensated, green_compensated, blue_compensated] + self._rgb = attributes["color"][0:3] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -207,14 +197,38 @@ class TasmotaLight( @property def rgb_color(self): """Return the rgb color value.""" - return self._rgb + if self._rgb is None: + return None + rgb = self._rgb + # Tasmota's RGB 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)) + else: + red_compensated = 0 + green_compensated = 0 + 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 - return [*self._rgb, self._white_value] + 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): @@ -250,18 +264,10 @@ class TasmotaLight( 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 otheruse - if rgbw[3] == 0: - 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 + # is on, and to color otherwise if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a6e7a1d45a8..15b5501adce 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.12"], + "requirements": ["hatasmota==0.2.13"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 63457206d6e..2046d448902 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91f7c1422ee..ca520da3349 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 74e8d2a5e59..44f57581694 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -36,7 +36,7 @@ DEFAULT_CONFIG = { "ofln": "Offline", "onln": "Online", "state": ["OFF", "ON", "TOGGLE", "HOLD"], - "sw": "8.4.0.2", + "sw": "9.4.0.4", "swn": [None, None, None, None, None], "t": "tasmota_49A3BC", "ft": "%topic%/%prefix%/", diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 3a27409e433..b74799d1d12 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -197,7 +197,7 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 - config["lt_st"] = 4 # 5 channel light (RGBW) + config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] async_fire_mqtt_message( @@ -406,6 +406,99 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("color_mode") == "color_temp" +async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + 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() + + 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") == "rgbw" + + 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}' + ) + 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" + + async_fire_mqtt_message( + 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("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" + + 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("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" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' + ) + 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" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -667,7 +760,17 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 0, 0) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -799,9 +902,10 @@ 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): +async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) + config["sw"] = "9.4.0.3" # RGBW support was added in 9.4.0.4 config["rl"][0] = 2 config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] @@ -895,6 +999,102 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +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;Dimmer4 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 white is off + 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", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set white when white is on + 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", + 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)