From b45fa29077e44c538a79cc4269be1421c8d27245 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Nov 2020 12:51:01 +0100 Subject: [PATCH] Force color or white mode exclusivity for MQTT lights (#42887) --- .../components/mqtt/light/schema_basic.py | 14 +- tests/components/mqtt/test_light.py | 166 ++++++++++++------ 2 files changed, 127 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 6fa6d70a665..4796652f57e 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -546,12 +546,21 @@ class MqttLight( @property def hs_color(self): """Return the hs color value.""" + if self._white_value: + return None return self._hs @property def color_temp(self): """Return the color temperature in mired.""" - return self._color_temp + supports_color = ( + self._topic[CONF_RGB_COMMAND_TOPIC] + or self._topic[CONF_HS_COMMAND_TOPIC] + or self._topic[CONF_XY_COMMAND_TOPIC] + ) + if self._white_value or not supports_color: + return self._color_temp + return None @property def min_mireds(self): @@ -569,7 +578,8 @@ class MqttLight( white_value = self._white_value if white_value: white_value = min(round(white_value), 255) - return white_value + return white_value + return None @property def should_poll(self): diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 102d1726ccc..9e044380555 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -311,21 +311,26 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): 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 + + 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 async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") light_state = hass.states.get("light.test") assert light_state.attributes["effect"] == "rainbow" - 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 - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") 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") is None + + 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) @@ -385,18 +390,16 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, "test_light_rgb/status", "1") 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/color_temp/status", "153") async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "255") 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") == 153 + 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("white_value") == 255 + assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") == (0.323, 0.329) async_fire_mqtt_message(hass, "test_light_rgb/status", "") @@ -409,21 +412,11 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): light_state = hass.states.get("light.test") assert light_state.attributes["brightness"] == 255 - 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_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/white_value/status", "") - assert "Ignoring empty white value message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 255 - 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") @@ -444,6 +437,29 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): 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/color_temp/status", "153") + async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "255") + + 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("white_value") == 255 + 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_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "") + assert "Ignoring empty white value message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["white_value"] == 255 + async def test_brightness_controlling_scale(hass, mqtt_mock): """Test the brightness controlling scale.""" @@ -628,6 +644,15 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): 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") == (84, 169, 255) + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") == "rainbow" + assert state.attributes.get("white_value") is None + async_fire_mqtt_message( hass, "test_light_rgb/white_value/status", '{"hello": "75"}' ) @@ -635,12 +660,13 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 50 - assert state.attributes.get("rgb_color") == (84, 169, 255) + assert state.attributes.get("rgb_color") is None assert state.attributes.get("color_temp") == 300 assert state.attributes.get("effect") == "rainbow" assert state.attributes.get("white_value") == 75 async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') + async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", '{"hello": "0"}') state = hass.states.get("light.test") assert state.attributes.get("hs_color") == (100, 50) @@ -710,7 +736,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "hs_color": [100, 100], "effect": "random", "color_temp": 100, - "white_value": 50, + # TODO: Test restoring state with white_value + "white_value": 0, }, ) with patch( @@ -726,8 +753,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 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") == 100 - assert state.attributes.get("white_value") == 50 + assert state.attributes.get("color_temp") is None + assert state.attributes.get("white_value") is None assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_on(hass, "light.test") @@ -754,7 +781,6 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) mqtt_mock.async_publish.assert_has_calls( [ @@ -762,9 +788,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): call("test_light_rgb/rgb/set", "255,128,0", 2, False), call("test_light_rgb/brightness/set", "50", 2, False), call("test_light_rgb/hs/set", "359.0,78.0", 2, False), - call("test_light_rgb/white_value/set", "80", 2, False), call("test_light_rgb/xy/set", "0.14,0.131", 2, False), - call("test_light_rgb/color_temp/set", "125", 2, False), ], any_order=True, ) @@ -774,8 +798,28 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["rgb_color"] == (255, 128, 0) assert state.attributes["brightness"] == 50 assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes["white_value"] == 80 + assert state.attributes.get("white_value") is None assert state.attributes["xy_color"] == (0.611, 0.375) + assert state.attributes.get("color_temp") is None + + await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/white_value/set", "80", 2, False), + call("test_light_rgb/color_temp/set", "125", 2, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes["brightness"] == 50 + assert state.attributes.get("hs_color") is None + assert state.attributes["white_value"] == 80 + assert state.attributes.get("xy_color") is None + assert state.attributes["color_temp"] == 125 async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): @@ -1385,11 +1429,16 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state1", - '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + '{"state1":{"state":"ON", "brightness":100, "ct":123, "white":100, "fx":"cycle"}}', ) ], "on", - [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + [ + ("brightness", 100), + ("color_temp", 123), + ("white_value", 100), + ("effect", "cycle"), + ], ), ( [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], @@ -1400,11 +1449,11 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state1", - '{"state1":{"state":"ON", "hs":"1,2"}}', + '{"state1":{"state":"ON", "hs":"1,2", "white":0}}', ) ], "on", - [("hs_color", (1, 2))], + [("hs_color", (1, 2)), ("white_value", None)], ), ( [ @@ -1420,11 +1469,11 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state1", - '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + '{"state1":{"xy":"0.3, 0.4"}}', ) ], "on", - [("white_value", 50), ("xy_color", (0.3, 0.401))], + [("xy_color", (0.3, 0.401))], ), ] state_data2 = [ @@ -1432,11 +1481,16 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state2", - '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + '{"state2":{"state":"ON", "brightness":50, "ct":200, "white":50, "fx":"loop"}}', ) ], "on", - [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + [ + ("brightness", 50), + ("color_temp", 200), + ("white_value", 50), + ("effect", "loop"), + ], ), ( [ @@ -1480,11 +1534,11 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state2", - '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + '{"state2":{"state":"ON", "hs":"1.2,2.2", "white":0}}', ) ], "on", - [("hs_color", (1.2, 2.2))], + [("hs_color", (1.2, 2.2)), ("white_value", None)], ), ( [ @@ -1536,11 +1590,11 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog [ ( "test_light_rgb/state2", - '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + '{"state2":{"xy":"0.4, 0.3"}}', ) ], "on", - [("white_value", 75), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ( [ @@ -1558,7 +1612,7 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog ), ], "on", - [("white_value", 75), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ] @@ -1640,11 +1694,16 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + '{"state1":{"state":"ON", "brightness":100, "ct":123, "white":100, "fx":"cycle"}}', ) ], "on", - [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + [ + ("brightness", 100), + ("color_temp", 123), + ("white_value", 100), + ("effect", "cycle"), + ], ), ( [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], @@ -1655,7 +1714,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state1":{"state":"ON", "hs":"1,2"}}', + '{"state1":{"state":"ON", "hs":"1,2", "white":0}}', ) ], "on", @@ -1675,11 +1734,11 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + '{"state1":{"white":0, "xy":"0.3, 0.4"}}', ) ], "on", - [("white_value", 50), ("xy_color", (0.3, 0.401))], + [("white_value", None), ("xy_color", (0.3, 0.401))], ), ] state_data2 = [ @@ -1687,11 +1746,16 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + '{"state2":{"state":"ON", "brightness":50, "ct":200, "white":50, "fx":"loop"}}', ) ], "on", - [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + [ + ("brightness", 50), + ("color_temp", 200), + ("white_value", 50), + ("effect", "loop"), + ], ), ( [ @@ -1717,7 +1781,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + '{"state2":{"state":"ON", "hs":"1.2,2.2", "white":0}}', ) ], "on", @@ -1757,11 +1821,11 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): [ ( "test_light_rgb/state1", - '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + '{"state2":{"xy":"0.4, 0.3"}}', ) ], "on", - [("white_value", 75), ("xy_color", (0.4, 0.3))], + [("white_value", None), ("xy_color", (0.4, 0.3))], ), ( [ @@ -1771,7 +1835,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): ) ], "on", - [("white_value", 75), ("xy_color", (0.4, 0.3))], + [("white_value", None), ("xy_color", (0.4, 0.3))], ), ]