From cabdae98e806dc45f21ea9cb55ad49e6efad6a03 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 9 Jan 2025 18:34:42 +0100 Subject: [PATCH] Allow to process kelvin as color_temp for mqtt json light (#133955) --- homeassistant/components/mqtt/const.py | 3 + .../components/mqtt/light/schema_basic.py | 6 +- .../components/mqtt/light/schema_json.py | 26 +- tests/components/mqtt/test_light_json.py | 242 +++++++++++++++--- 4 files changed, 235 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 9f1c55a54e0..db27495154b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -56,12 +56,15 @@ CONF_SUPPORTED_FEATURES = "supported_features" CONF_ACTION_TEMPLATE = "action_template" CONF_ACTION_TOPIC = "action_topic" +CONF_COLOR_TEMP_KELVIN = "color_temp_kelvin" CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_ENABLED_BY_DEFAULT = "enabled_by_default" CONF_ENTITY_PICTURE = "entity_picture" +CONF_MAX_KELVIN = "max_kelvin" +CONF_MIN_KELVIN = "min_kelvin" CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 632c651e3a5..3234e9a2986 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -51,7 +51,10 @@ import homeassistant.util.color as color_util from .. import subscription from ..config import MQTT_RW_SCHEMA from ..const import ( + CONF_COLOR_TEMP_KELVIN, CONF_COMMAND_TOPIC, + CONF_MAX_KELVIN, + CONF_MIN_KELVIN, CONF_STATE_TOPIC, CONF_STATE_VALUE_TEMPLATE, PAYLOAD_NONE, @@ -82,7 +85,6 @@ CONF_COLOR_TEMP_COMMAND_TEMPLATE = "color_temp_command_template" CONF_COLOR_TEMP_COMMAND_TOPIC = "color_temp_command_topic" CONF_COLOR_TEMP_STATE_TOPIC = "color_temp_state_topic" CONF_COLOR_TEMP_VALUE_TEMPLATE = "color_temp_value_template" -CONF_COLOR_TEMP_KELVIN = "color_temp_kelvin" CONF_EFFECT_COMMAND_TEMPLATE = "effect_command_template" CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic" CONF_EFFECT_LIST = "effect_list" @@ -94,8 +96,6 @@ CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" -CONF_MAX_KELVIN = "max_kelvin" -CONF_MIN_KELVIN = "min_kelvin" CONF_RGB_COMMAND_TEMPLATE = "rgb_command_template" CONF_RGB_COMMAND_TOPIC = "rgb_command_topic" CONF_RGB_STATE_TOPIC = "rgb_state_topic" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index f6efdd3281d..2d152ca12c8 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -61,7 +61,10 @@ from homeassistant.util.yaml import dump as yaml_dump from .. import subscription from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA from ..const import ( + CONF_COLOR_TEMP_KELVIN, CONF_COMMAND_TOPIC, + CONF_MAX_KELVIN, + CONF_MIN_KELVIN, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, @@ -203,6 +206,7 @@ _PLATFORM_SCHEMA_BASE = ( # CONF_COLOR_TEMP was deprecated with HA Core 2024.4 and will be # removed with HA Core 2025.3 vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, + vol.Optional(CONF_COLOR_TEMP_KELVIN, default=False): cv.boolean, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional( @@ -216,6 +220,8 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, + vol.Optional(CONF_MAX_KELVIN): cv.positive_int, + vol.Optional(CONF_MIN_KELVIN): cv.positive_int, vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) @@ -275,15 +281,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._color_temp_kelvin = config[CONF_COLOR_TEMP_KELVIN] self._attr_min_color_temp_kelvin = ( color_util.color_temperature_mired_to_kelvin(max_mireds) if (max_mireds := config.get(CONF_MAX_MIREDS)) - else DEFAULT_MIN_KELVIN + else config.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) ) self._attr_max_color_temp_kelvin = ( color_util.color_temperature_mired_to_kelvin(min_mireds) if (min_mireds := config.get(CONF_MIN_MIREDS)) - else DEFAULT_MAX_KELVIN + else config.get(CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN) ) self._attr_effect_list = config.get(CONF_EFFECT_LIST) @@ -381,7 +388,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): try: if color_mode == ColorMode.COLOR_TEMP: self._attr_color_temp_kelvin = ( - color_util.color_temperature_mired_to_kelvin( + values["color_temp"] + if self._color_temp_kelvin + else color_util.color_temperature_mired_to_kelvin( values["color_temp"] ) ) @@ -486,7 +495,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._attr_color_temp_kelvin = None else: self._attr_color_temp_kelvin = ( - color_util.color_temperature_mired_to_kelvin( + values["color_temp"] # type: ignore[assignment] + if self._color_temp_kelvin + else color_util.color_temperature_mired_to_kelvin( values["color_temp"] # type: ignore[arg-type] ) ) @@ -709,10 +720,13 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): should_update = True if ATTR_COLOR_TEMP_KELVIN in kwargs: - message["color_temp"] = color_util.color_temperature_kelvin_to_mired( + message["color_temp"] = ( kwargs[ATTR_COLOR_TEMP_KELVIN] + if self._color_temp_kelvin + else color_util.color_temperature_kelvin_to_mired( + kwargs[ATTR_COLOR_TEMP_KELVIN] + ) ) - if self._optimistic: self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_temp_kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN] diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index c127c86de39..512e4091438 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -14,7 +14,7 @@ mqtt: rgb: true xy: true -Configuration with RGB, brightness, color temp and effect: +Configuration with RGB, brightness, color temp (mireds) and effect: mqtt: light: @@ -24,10 +24,11 @@ mqtt: command_topic: "home/rgb1/set" brightness: true color_temp: true + color_temp_kelvin: false effect: true rgb: true -Configuration with RGB, brightness and color temp: +Configuration with RGB, brightness and color temp (Kelvin): mqtt: light: @@ -38,6 +39,7 @@ mqtt: brightness: true rgb: true color_temp: true + color_temp_kelvin: true Configuration with RGB, brightness: @@ -399,24 +401,50 @@ async def test_fail_setup_if_color_modes_invalid( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "kelvin", "color_temp_payload_value"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_light/set", - "state_topic": "test_light", - "color_mode": True, - "supported_color_modes": "color_temp", + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_light/set", + "state_topic": "test_light", + "color_mode": True, + "color_temp_kelvin": False, + "supported_color_modes": "color_temp", + } } - } - } + }, + 5208, + 192, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_light/set", + "state_topic": "test_light", + "color_mode": True, + "color_temp_kelvin": True, + "supported_color_modes": "color_temp", + } + } + }, + 5208, + 5208, + ), ], + ids=["mireds", "kelvin"], ) async def test_single_color_mode( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + kelvin: int, + color_temp_payload_value: int, ) -> None: """Test setup with single color_mode.""" await mqtt_mock_entry() @@ -424,13 +452,19 @@ async def test_single_color_mode( assert state.state == STATE_UNKNOWN await common.async_turn_on( - hass, "light.test", brightness=50, color_temp_kelvin=5208 + hass, "light.test", brightness=50, color_temp_kelvin=kelvin ) + payload = { + "state": "ON", + "brightness": 50, + "color_mode": "color_temp", + "color_temp": color_temp_payload_value, + } async_fire_mqtt_message( hass, "test_light", - '{"state": "ON", "brightness": 50, "color_mode": "color_temp", "color_temp": 192}', + json_dumps(payload), ) color_modes = [light.ColorMode.COLOR_TEMP] state = hass.states.get("light.test") @@ -788,6 +822,96 @@ async def test_controlling_state_via_topic( assert light_state.attributes.get("brightness") == 128 +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "state_topic": "test_light_rgb", + "command_topic": "test_light_rgb/set", + "brightness": True, + "color_temp": True, + "color_temp_kelvin": True, + "effect": True, + "rgb": True, + "xy": True, + "hs": True, + "qos": "0", + } + } + } + ], +) +async def test_controlling_state_color_temp_kelvin( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test the controlling of the state via topic in Kelvin mode.""" + await mqtt_mock_entry() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + color_modes = [light.ColorMode.COLOR_TEMP, light.ColorMode.HS] + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + expected_features = ( + light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION + ) + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp_kelvin") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get("hs_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Turn on the light + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON",' + '"color":{"r":255,"g":255,"b":255},' + '"brightness":255,' + '"color_temp":155,' + '"effect":"colorloop"}', + ) + + 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_kelvin") is None # rgb color has priority + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get("xy_color") == (0.323, 0.329) + assert state.attributes.get("hs_color") == (0.0, 0.0) + + # Turn on the light + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON",' + '"brightness":255,' + '"color":null,' + '"color_temp":6451,' # Kelvin + '"effect":"colorloop"}', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == ( + 255, + 253, + 249, + ) # temp converted to color + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp_kelvin") == 6451 + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get("xy_color") == (0.328, 0.333) # temp converted to color + assert state.attributes.get("hs_color") == (44.098, 2.43) # temp converted to color + + @pytest.mark.parametrize( "hass_config", [ @@ -2591,30 +2715,82 @@ async def test_entity_debug_info_message( @pytest.mark.parametrize( - "hass_config", + ("hass_config", "min_kelvin", "max_kelvin"), [ - { - mqtt.DOMAIN: { - light.DOMAIN: { - "schema": "json", - "name": "test", - "command_topic": "test_max_mireds/set", - "color_temp": True, - "max_mireds": 370, + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "max_mireds": 370, # 2702 Kelvin + } } - } - } + }, + 2702, + light.DEFAULT_MAX_KELVIN, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "min_mireds": 150, # 6666 Kelvin + } + } + }, + light.DEFAULT_MIN_KELVIN, + 6666, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "min_kelvin": 2702, + } + } + }, + 2702, + light.DEFAULT_MAX_KELVIN, + ), + ( + { + mqtt.DOMAIN: { + light.DOMAIN: { + "schema": "json", + "name": "test", + "command_topic": "test_max_mireds/set", + "supported_color_modes": ["color_temp"], + "max_kelvin": 6666, + } + } + }, + light.DEFAULT_MIN_KELVIN, + 6666, + ), ], ) -async def test_max_mireds( - hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +async def test_min_max_kelvin( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + min_kelvin: int, + max_kelvin: int, ) -> None: - """Test setting min_mireds and max_mireds.""" + """Test setting min_color_temp_kelvin and max_color_temp_kelvin.""" await mqtt_mock_entry() state = hass.states.get("light.test") - assert state.attributes.get("min_mireds") == 153 - assert state.attributes.get("max_mireds") == 370 + assert state.attributes.get("min_color_temp_kelvin") == min_kelvin + assert state.attributes.get("max_color_temp_kelvin") == max_kelvin @pytest.mark.parametrize(