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)