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