Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for color_mode white to MQTT light basic schema #51484

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions homeassistant/components/mqtt/abbreviations.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@
"uniq_id": "unique_id",
"unit_of_meas": "unit_of_measurement",
"val_tpl": "value_template",
"whit_cmd_t": "white_command_topic",
"whit_scl": "white_scale",
"whit_val_cmd_t": "white_value_command_topic",
"whit_val_scl": "white_value_scale",
"whit_val_stat_t": "white_value_state_topic",
Expand Down
55 changes: 43 additions & 12 deletions homeassistant/components/mqtt/light/schema_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_WHITE,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
COLOR_MODE_BRIGHTNESS,
Expand All @@ -22,13 +23,15 @@
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_UNKNOWN,
COLOR_MODE_WHITE,
COLOR_MODE_XY,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT,
SUPPORT_WHITE_VALUE,
LightEntity,
valid_supported_color_modes,
)
from homeassistant.const import (
CONF_NAME,
Expand Down Expand Up @@ -86,6 +89,8 @@
CONF_XY_COMMAND_TOPIC = "xy_command_topic"
CONF_XY_STATE_TOPIC = "xy_state_topic"
CONF_XY_VALUE_TEMPLATE = "xy_value_template"
CONF_WHITE_COMMAND_TOPIC = "white_command_topic"
CONF_WHITE_SCALE = "white_scale"
CONF_WHITE_VALUE_COMMAND_TOPIC = "white_value_command_topic"
CONF_WHITE_VALUE_SCALE = "white_value_scale"
CONF_WHITE_VALUE_STATE_TOPIC = "white_value_state_topic"
Expand All @@ -98,6 +103,7 @@
DEFAULT_PAYLOAD_OFF = "OFF"
DEFAULT_PAYLOAD_ON = "ON"
DEFAULT_WHITE_VALUE_SCALE = 255
DEFAULT_WHITE_SCALE = 255
DEFAULT_ON_COMMAND_TYPE = "last"

VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"]
Expand Down Expand Up @@ -168,6 +174,10 @@
vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(
Expand Down Expand Up @@ -259,6 +269,7 @@ def _setup_from_config(self, config):
CONF_RGBWW_COMMAND_TOPIC,
CONF_RGBWW_STATE_TOPIC,
CONF_STATE_TOPIC,
CONF_WHITE_COMMAND_TOPIC,
CONF_WHITE_VALUE_COMMAND_TOPIC,
CONF_WHITE_VALUE_STATE_TOPIC,
CONF_XY_COMMAND_TOPIC,
Expand Down Expand Up @@ -316,35 +327,40 @@ def _setup_from_config(self, config):
optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None
)
self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None
self._supported_color_modes = set()
supported_color_modes = set()
if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
self._color_mode = COLOR_MODE_COLOR_TEMP
if topic[CONF_HS_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_HS)
supported_color_modes.add(COLOR_MODE_HS)
self._color_mode = COLOR_MODE_HS
if topic[CONF_RGB_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_RGB)
supported_color_modes.add(COLOR_MODE_RGB)
self._color_mode = COLOR_MODE_RGB
if topic[CONF_RGBW_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_RGBW)
supported_color_modes.add(COLOR_MODE_RGBW)
self._color_mode = COLOR_MODE_RGBW
if topic[CONF_RGBWW_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_RGBWW)
supported_color_modes.add(COLOR_MODE_RGBWW)
self._color_mode = COLOR_MODE_RGBWW
if topic[CONF_WHITE_COMMAND_TOPIC] is not None:
supported_color_modes.add(COLOR_MODE_WHITE)
if topic[CONF_XY_COMMAND_TOPIC] is not None:
self._supported_color_modes.add(COLOR_MODE_XY)
supported_color_modes.add(COLOR_MODE_XY)
self._color_mode = COLOR_MODE_XY
if len(self._supported_color_modes) > 1:
if len(supported_color_modes) > 1:
self._color_mode = COLOR_MODE_UNKNOWN

if not self._supported_color_modes:
if not supported_color_modes:
if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None:
self._color_mode = COLOR_MODE_BRIGHTNESS
self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
else:
self._color_mode = COLOR_MODE_ONOFF
self._supported_color_modes.add(COLOR_MODE_ONOFF)
supported_color_modes.add(COLOR_MODE_ONOFF)

# Validate the color_modes configuration
self._supported_color_modes = valid_supported_color_modes(supported_color_modes)

if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None:
self._legacy_mode = True
Expand Down Expand Up @@ -817,7 +833,11 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None):
# If brightness is being used instead of an on command, make sure
# there is a brightness input. Either set the brightness to our
# saved value or the maximum value if this is the first call
elif on_command_type == "brightness" and ATTR_BRIGHTNESS not in kwargs:
elif (
on_command_type == "brightness"
and ATTR_BRIGHTNESS not in kwargs
and ATTR_WHITE not in kwargs
):
kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255

hs_color = kwargs.get(ATTR_HS_COLOR)
Expand Down Expand Up @@ -971,6 +991,17 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None):
publish(CONF_EFFECT_COMMAND_TOPIC, effect)
should_update |= set_optimistic(ATTR_EFFECT, effect)

if ATTR_WHITE in kwargs and self._topic[CONF_WHITE_COMMAND_TOPIC] is not None:
percent_white = float(kwargs[ATTR_WHITE]) / 255
white_scale = self._config[CONF_WHITE_SCALE]
device_white_value = min(round(percent_white * white_scale), white_scale)
publish(CONF_WHITE_COMMAND_TOPIC, device_white_value)
should_update |= set_optimistic(
ATTR_BRIGHTNESS,
kwargs[ATTR_WHITE],
COLOR_MODE_WHITE,
)

if (
ATTR_WHITE_VALUE in kwargs
and self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None
Expand Down
3 changes: 3 additions & 0 deletions tests/components/light/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_WHITE,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
DOMAIN,
Expand Down Expand Up @@ -92,6 +93,7 @@ async def async_turn_on(
flash=None,
effect=None,
color_name=None,
white=None,
):
"""Turn all or specified light on."""
data = {
Expand All @@ -113,6 +115,7 @@ async def async_turn_on(
(ATTR_FLASH, flash),
(ATTR_EFFECT, effect),
(ATTR_COLOR_NAME, color_name),
(ATTR_WHITE, white),
]
if value is not None
}
Expand Down
141 changes: 141 additions & 0 deletions tests/components/mqtt/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -2268,6 +2268,83 @@ async def test_on_command_rgbww_template(hass, mqtt_mock):
mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False)


async def test_on_command_white(hass, mqtt_mock):
"""Test sending commands for RGB + white light."""
config = {
light.DOMAIN: {
"platform": "mqtt",
"name": "test",
"command_topic": "tasmota_B94927/cmnd/POWER",
"value_template": "{{ value_json.POWER }}",
"payload_off": "OFF",
"payload_on": "ON",
"brightness_command_topic": "tasmota_B94927/cmnd/Dimmer",
"brightness_scale": 100,
"on_command_type": "brightness",
"brightness_value_template": "{{ value_json.Dimmer }}",
"rgb_command_topic": "tasmota_B94927/cmnd/Color2",
"rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}",
"white_command_topic": "tasmota_B94927/cmnd/White",
"white_scale": 100,
"color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}",
"qos": "0",
}
}
color_modes = ["rgb", "white"]

assert await async_setup_component(hass, light.DOMAIN, config)
await hass.async_block_till_done()

state = hass.states.get("light.test")
assert state.state == STATE_OFF
assert state.attributes.get("brightness") is None
assert state.attributes.get("rgb_color") is None
assert state.attributes.get(light.ATTR_COLOR_MODE) is None
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes
assert state.attributes.get(ATTR_ASSUMED_STATE)

await common.async_turn_on(hass, "light.test", brightness=192)
mqtt_mock.async_publish.assert_has_calls(
[
call("tasmota_B94927/cmnd/Dimmer", "75", 0, False),
],
any_order=True,
)
mqtt_mock.async_publish.reset_mock()

await common.async_turn_on(hass, "light.test", white=255)
mqtt_mock.async_publish.assert_has_calls(
[
call("tasmota_B94927/cmnd/White", "100", 0, False),
],
any_order=True,
)
mqtt_mock.async_publish.reset_mock()

await common.async_turn_on(hass, "light.test", white=64)
mqtt_mock.async_publish.assert_has_calls(
[
call("tasmota_B94927/cmnd/White", "25", 0, False),
],
any_order=True,
)
mqtt_mock.async_publish.reset_mock()

await common.async_turn_on(hass, "light.test")
mqtt_mock.async_publish.assert_has_calls(
[
call("tasmota_B94927/cmnd/Dimmer", "25", 0, False),
],
any_order=True,
)
mqtt_mock.async_publish.reset_mock()

await common.async_turn_off(hass, "light.test")
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_B94927/cmnd/POWER", "OFF", 0, False
)


async def test_explicit_color_mode(hass, mqtt_mock):
"""Test explicit color mode over mqtt."""
config = {
Expand Down Expand Up @@ -2499,6 +2576,70 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock):
assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes


async def test_white_state_update(hass, mqtt_mock):
"""Test state updates for RGB + white light."""
config = {
light.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "tasmota_B94927/tele/STATE",
"command_topic": "tasmota_B94927/cmnd/POWER",
"value_template": "{{ value_json.POWER }}",
"payload_off": "OFF",
"payload_on": "ON",
"brightness_command_topic": "tasmota_B94927/cmnd/Dimmer",
"brightness_state_topic": "tasmota_B94927/tele/STATE",
"brightness_scale": 100,
"on_command_type": "brightness",
"brightness_value_template": "{{ value_json.Dimmer }}",
"rgb_command_topic": "tasmota_B94927/cmnd/Color2",
"rgb_state_topic": "tasmota_B94927/tele/STATE",
"rgb_value_template": "{{value_json.Color.split(',')[0:3]|join(',')}}",
"white_command_topic": "tasmota_B94927/cmnd/White",
"white_scale": 100,
"color_mode_state_topic": "tasmota_B94927/tele/STATE",
"color_mode_value_template": "{% if value_json.White %} white {% else %} rgb {% endif %}",
"qos": "0",
}
}
color_modes = ["rgb", "white"]

assert await async_setup_component(hass, light.DOMAIN, config)
await hass.async_block_till_done()

state = hass.states.get("light.test")
assert state.state == STATE_OFF
assert state.attributes.get("brightness") is None
assert state.attributes.get("rgb_color") 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,
"tasmota_B94927/tele/STATE",
'{"POWER":"ON","Dimmer":50,"Color":"0,0,0,128","White":50}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 128
assert state.attributes.get("rgb_color") is None
assert state.attributes.get(light.ATTR_COLOR_MODE) == "white"
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes

async_fire_mqtt_message(
hass,
"tasmota_B94927/tele/STATE",
'{"POWER":"ON","Dimmer":50,"Color":"128,64,32,0","White":0}',
)
state = hass.states.get("light.test")
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 128
assert state.attributes.get("rgb_color") == (128, 64, 32)
assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb"
assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes


async def test_effect(hass, mqtt_mock):
"""Test effect."""
config = {
Expand Down