From ec37e1ff8d1daac5137edf15dabbbe46886de25a Mon Sep 17 00:00:00 2001
From: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Date: Thu, 9 Jan 2025 16:31:09 +0100
Subject: [PATCH] Allow to process kelvin as color_temp for mqtt basic light
 (#133953)

---
 .../components/mqtt/abbreviations.py          |   3 +
 .../components/mqtt/light/schema_basic.py     |  20 +-
 tests/components/mqtt/test_light.py           | 238 ++++++++++++++++--
 3 files changed, 241 insertions(+), 20 deletions(-)

diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py
index 65e24d5d780..584b238b3a8 100644
--- a/homeassistant/components/mqtt/abbreviations.py
+++ b/homeassistant/components/mqtt/abbreviations.py
@@ -23,6 +23,7 @@ ABBREVIATIONS = {
     "clrm_stat_t": "color_mode_state_topic",
     "clrm_val_tpl": "color_mode_value_template",
     "clr_temp_cmd_t": "color_temp_command_topic",
+    "clr_temp_k": "color_temp_kelvin",
     "clr_temp_stat_t": "color_temp_state_topic",
     "clr_temp_tpl": "color_temp_template",
     "clr_temp_val_tpl": "color_temp_value_template",
@@ -92,6 +93,8 @@ ABBREVIATIONS = {
     "min_hum": "min_humidity",
     "max_mirs": "max_mireds",
     "min_mirs": "min_mireds",
+    "max_k": "max_kelvin",
+    "min_k": "min_kelvin",
     "max_temp": "max_temp",
     "min_temp": "min_temp",
     "migr_discvry": "migrate_discovery",
diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py
index 159a23d14d9..632c651e3a5 100644
--- a/homeassistant/components/mqtt/light/schema_basic.py
+++ b/homeassistant/components/mqtt/light/schema_basic.py
@@ -82,6 +82,7 @@ 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"
@@ -93,6 +94,8 @@ 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"
@@ -182,6 +185,7 @@ PLATFORM_SCHEMA_MODERN_BASIC = (
             vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): valid_publish_topic,
             vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): valid_subscribe_topic,
             vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
+            vol.Optional(CONF_COLOR_TEMP_KELVIN, default=False): cv.boolean,
             vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template,
             vol.Optional(CONF_EFFECT_COMMAND_TOPIC): valid_publish_topic,
             vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
@@ -193,6 +197,8 @@ PLATFORM_SCHEMA_MODERN_BASIC = (
             vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template,
             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_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In(
                 VALUES_ON_COMMAND_TYPE
@@ -239,6 +245,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
     _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
     _topic: dict[str, str | None]
     _payload: dict[str, str]
+    _color_temp_kelvin: bool
     _command_templates: dict[
         str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType]
     ]
@@ -263,16 +270,18 @@ class MqttLight(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)
 
         topic: dict[str, str | None] = {
@@ -526,6 +535,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
 
         if self._optimistic_color_mode:
             self._attr_color_mode = ColorMode.COLOR_TEMP
+        if self._color_temp_kelvin:
+            self._attr_color_temp_kelvin = int(payload)
+            return
         self._attr_color_temp_kelvin = color_util.color_temperature_mired_to_kelvin(
             int(payload)
         )
@@ -818,7 +830,9 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
         ):
             ct_command_tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE]
             color_temp = ct_command_tpl(
-                color_util.color_temperature_kelvin_to_mired(
+                kwargs[ATTR_COLOR_TEMP_KELVIN]
+                if self._color_temp_kelvin
+                else color_util.color_temperature_kelvin_to_mired(
                     kwargs[ATTR_COLOR_TEMP_KELVIN]
                 ),
                 None,
diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py
index dbca09e803c..f8c66a3de1d 100644
--- a/tests/components/mqtt/test_light.py
+++ b/tests/components/mqtt/test_light.py
@@ -72,7 +72,7 @@ mqtt:
         payload_on: "on"
         payload_off: "off"
 
-config with brightness and color temp
+config with brightness and color temp (mired)
 
 mqtt:
     light:
@@ -88,6 +88,23 @@ mqtt:
         payload_on: "on"
         payload_off: "off"
 
+config with brightness and color temp (Kelvin)
+
+mqtt:
+    light:
+      - name: "Office Light Color Temp"
+        state_topic: "office/rgb1/light/status"
+        command_topic: "office/rgb1/light/switch"
+        brightness_state_topic: "office/rgb1/brightness/status"
+        brightness_command_topic: "office/rgb1/brightness/set"
+        brightness_scale: 99
+        color_temp_kelvin: true
+        color_temp_state_topic: "office/rgb1/color_temp/status"
+        color_temp_command_topic: "office/rgb1/color_temp/set"
+        qos: 0
+        payload_on: "on"
+        payload_off: "off"
+
 config with brightness and effect
 
 mqtt:
@@ -305,6 +322,101 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(
     assert state.state == STATE_UNKNOWN
 
 
+@pytest.mark.parametrize(
+    ("hass_config", "min_kelvin", "max_kelvin"),
+    [
+        (
+            help_custom_config(
+                light.DOMAIN,
+                DEFAULT_CONFIG,
+                (
+                    {
+                        "color_temp_state_topic": "test_light_rgb/color_temp/status",
+                        "color_temp_command_topic": "test_light_rgb/color_temp/set",
+                    },
+                ),
+            ),
+            light.DEFAULT_MIN_KELVIN,
+            light.DEFAULT_MAX_KELVIN,
+        ),
+        (
+            help_custom_config(
+                light.DOMAIN,
+                DEFAULT_CONFIG,
+                (
+                    {
+                        "color_temp_state_topic": "test_light_rgb/color_temp/status",
+                        "color_temp_command_topic": "test_light_rgb/color_temp/set",
+                        "min_mireds": 180,
+                    },
+                ),
+            ),
+            light.DEFAULT_MIN_KELVIN,
+            5555,
+        ),
+        (
+            help_custom_config(
+                light.DOMAIN,
+                DEFAULT_CONFIG,
+                (
+                    {
+                        "color_temp_state_topic": "test_light_rgb/color_temp/status",
+                        "color_temp_command_topic": "test_light_rgb/color_temp/set",
+                        "max_mireds": 400,
+                    },
+                ),
+            ),
+            2500,
+            light.DEFAULT_MAX_KELVIN,
+        ),
+        (
+            help_custom_config(
+                light.DOMAIN,
+                DEFAULT_CONFIG,
+                (
+                    {
+                        "color_temp_state_topic": "test_light_rgb/color_temp/status",
+                        "color_temp_command_topic": "test_light_rgb/color_temp/set",
+                        "max_kelvin": 5555,
+                    },
+                ),
+            ),
+            light.DEFAULT_MIN_KELVIN,
+            5555,
+        ),
+        (
+            help_custom_config(
+                light.DOMAIN,
+                DEFAULT_CONFIG,
+                (
+                    {
+                        "color_temp_state_topic": "test_light_rgb/color_temp/status",
+                        "color_temp_command_topic": "test_light_rgb/color_temp/set",
+                        "min_kelvin": 2500,
+                    },
+                ),
+            ),
+            2500,
+            light.DEFAULT_MAX_KELVIN,
+        ),
+    ],
+)
+async def test_no_min_max_kelvin(
+    hass: HomeAssistant,
+    mqtt_mock_entry: MqttMockHAClientGenerator,
+    min_kelvin: int,
+    max_kelvin: int,
+) -> None:
+    """Test if there is no color and brightness if no topic."""
+    await mqtt_mock_entry()
+
+    async_fire_mqtt_message(hass, "test-topic", "ON")
+    state = hass.states.get("light.test")
+    assert state is not None and state.state == STATE_UNKNOWN
+    assert state.attributes.get(light.ATTR_MIN_COLOR_TEMP_KELVIN) == min_kelvin
+    assert state.attributes.get(light.ATTR_MAX_COLOR_TEMP_KELVIN) == max_kelvin
+
+
 @pytest.mark.parametrize(
     "hass_config",
     [
@@ -431,6 +543,76 @@ async def test_controlling_state_via_topic(
     assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
 
 
+@pytest.mark.parametrize(
+    ("hass_config", "payload", "kelvin"),
+    [
+        (
+            {
+                mqtt.DOMAIN: {
+                    light.DOMAIN: {
+                        "name": "test",
+                        "state_topic": "test_light_color_temp/status",
+                        "command_topic": "test_light_color_temp/set",
+                        "brightness_state_topic": "test_light_color_temp/brightness/status",
+                        "brightness_command_topic": "test_light_color_temp/brightness/set",
+                        "color_temp_state_topic": "test_light_color_temp/color_temp/status",
+                        "color_temp_command_topic": "test_light_color_temp/color_temp/set",
+                        "color_temp_kelvin": False,
+                    }
+                }
+            },
+            "300",
+            3333,
+        ),
+        (
+            {
+                mqtt.DOMAIN: {
+                    light.DOMAIN: {
+                        "name": "test",
+                        "state_topic": "test_light_color_temp/status",
+                        "command_topic": "test_light_color_temp/set",
+                        "brightness_state_topic": "test_light_color_temp/brightness/status",
+                        "brightness_command_topic": "test_light_color_temp/brightness/set",
+                        "color_temp_state_topic": "test_light_color_temp/color_temp/status",
+                        "color_temp_command_topic": "test_light_color_temp/color_temp/set",
+                        "color_temp_kelvin": True,
+                    }
+                }
+            },
+            "3333",
+            3333,
+        ),
+    ],
+    ids=["mireds", "kelvin"],
+)
+async def test_controlling_color_mode_state_via_topic(
+    hass: HomeAssistant,
+    mqtt_mock_entry: MqttMockHAClientGenerator,
+    payload: str,
+    kelvin: int,
+) -> None:
+    """Test the controlling of the color mode state via topic."""
+    color_modes = ["color_temp"]
+
+    await mqtt_mock_entry()
+
+    state = hass.states.get("light.test")
+    assert state.state == STATE_UNKNOWN
+    assert state.attributes.get("color_temp_kelvin") is None
+    assert state.attributes.get(light.ATTR_COLOR_MODE) is None
+    assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+    assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+    async_fire_mqtt_message(hass, "test_light_color_temp/status", "ON")
+    async_fire_mqtt_message(hass, "test_light_color_temp/brightness/status", "70")
+    async_fire_mqtt_message(hass, "test_light_color_temp/color_temp/status", payload)
+    light_state = hass.states.get("light.test")
+    assert light_state.attributes.get("brightness") == 70
+    assert light_state.attributes["color_temp_kelvin"] == kelvin
+    assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp"
+    assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
+
+
 @pytest.mark.parametrize(
     "hass_config",
     [
@@ -1295,25 +1477,47 @@ async def test_sending_mqtt_rgbww_command_with_template(
 
 
 @pytest.mark.parametrize(
-    "hass_config",
+    ("hass_config", "payload"),
     [
-        {
-            mqtt.DOMAIN: {
-                light.DOMAIN: {
-                    "name": "test",
-                    "command_topic": "test_light_color_temp/set",
-                    "color_temp_command_topic": "test_light_color_temp/color_temp/set",
-                    "color_temp_command_template": "{{ (1000 / value) | round(0) }}",
-                    "payload_on": "on",
-                    "payload_off": "off",
-                    "qos": 0,
+        (
+            {
+                mqtt.DOMAIN: {
+                    light.DOMAIN: {
+                        "name": "test",
+                        "command_topic": "test_light_color_temp/set",
+                        "color_temp_command_topic": "test_light_color_temp/color_temp/set",
+                        "color_temp_command_template": "{{ (1000 / value) | round(0) }}",
+                        "color_temp_kelvin": False,
+                        "payload_on": "on",
+                        "payload_off": "off",
+                        "qos": 0,
+                    }
                 }
-            }
-        }
+            },
+            "10",
+        ),
+        (
+            {
+                mqtt.DOMAIN: {
+                    light.DOMAIN: {
+                        "name": "test",
+                        "command_topic": "test_light_color_temp/set",
+                        "color_temp_command_topic": "test_light_color_temp/color_temp/set",
+                        "color_temp_command_template": "{{ (0.5 * value) | round(0) }}",
+                        "color_temp_kelvin": True,
+                        "payload_on": "on",
+                        "payload_off": "off",
+                        "qos": 0,
+                    }
+                }
+            },
+            "5000",
+        ),
     ],
+    ids=["mireds", "kelvin"],
 )
 async def test_sending_mqtt_color_temp_command_with_template(
-    hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
+    hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, payload: str
 ) -> None:
     """Test the sending of Color Temp command with template."""
     mqtt_mock = await mqtt_mock_entry()
@@ -1326,14 +1530,14 @@ async def test_sending_mqtt_color_temp_command_with_template(
     mqtt_mock.async_publish.assert_has_calls(
         [
             call("test_light_color_temp/set", "on", 0, False),
-            call("test_light_color_temp/color_temp/set", "10", 0, False),
+            call("test_light_color_temp/color_temp/set", payload, 0, False),
         ],
         any_order=True,
     )
 
     state = hass.states.get("light.test")
     assert state.state == STATE_ON
-    assert state.attributes["color_temp"] == 100
+    assert state.attributes["color_temp_kelvin"] == 10000
 
 
 @pytest.mark.parametrize(
-- 
GitLab