Add support for HS color to mqtt light (#16958)

* Add support for HS color to mqtt light

* Warn if hs state update is invalid
This commit is contained in:
emontnemery 2018-10-08 15:36:57 +02:00 committed by Paulus Schoutsen
parent 9290f245bf
commit 42fb886d71
3 changed files with 133 additions and 7 deletions

View File

@ -15,7 +15,7 @@ from homeassistant.components.light import (
ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE)
from homeassistant.const import (
CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME,
CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_HS, CONF_NAME,
CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON,
CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY)
from homeassistant.components.mqtt import (
@ -44,6 +44,9 @@ CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic'
CONF_EFFECT_LIST = 'effect_list'
CONF_EFFECT_STATE_TOPIC = 'effect_state_topic'
CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template'
CONF_HS_COMMAND_TOPIC = 'hs_command_topic'
CONF_HS_STATE_TOPIC = 'hs_state_topic'
CONF_HS_VALUE_TEMPLATE = 'hs_value_template'
CONF_RGB_COMMAND_TEMPLATE = 'rgb_command_template'
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
@ -82,6 +85,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
@ -143,6 +149,8 @@ async def _async_setup_entity(hass, config, async_add_entities,
CONF_COMMAND_TOPIC,
CONF_EFFECT_COMMAND_TOPIC,
CONF_EFFECT_STATE_TOPIC,
CONF_HS_COMMAND_TOPIC,
CONF_HS_STATE_TOPIC,
CONF_RGB_COMMAND_TOPIC,
CONF_RGB_STATE_TOPIC,
CONF_STATE_TOPIC,
@ -156,6 +164,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE),
CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE),
CONF_HS: config.get(CONF_HS_VALUE_TEMPLATE),
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE),
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
@ -207,6 +216,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None)
self._optimistic_effect = (
optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None)
self._optimistic_hs = \
optimistic or topic[CONF_HS_STATE_TOPIC] is None
self._optimistic_white_value = (
optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None)
self._optimistic_xy = \
@ -232,6 +243,8 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._supported_features |= (
topic[CONF_EFFECT_COMMAND_TOPIC] is not None and
SUPPORT_EFFECT)
self._supported_features |= (
topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR)
self._supported_features |= (
topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and
SUPPORT_WHITE_VALUE)
@ -374,6 +387,33 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
else:
self._effect = None
@callback
def hs_received(topic, payload, qos):
"""Handle new MQTT messages for hs color."""
payload = templates[CONF_HS](payload)
if not payload:
_LOGGER.debug("Ignoring empty hs message from '%s'", topic)
return
try:
hs_color = [float(val) for val in payload.split(',', 2)]
self._hs = hs_color
self.async_schedule_update_ha_state()
except ValueError:
_LOGGER.debug("Failed to parse hs state update: '%s'",
payload)
if self._topic[CONF_HS_STATE_TOPIC] is not None:
await mqtt.async_subscribe(
self.hass, self._topic[CONF_HS_STATE_TOPIC], hs_received,
self._qos)
self._hs = (0, 0)
if self._optimistic_hs and last_state\
and last_state.attributes.get(ATTR_HS_COLOR):
self._hs = last_state.attributes.get(ATTR_HS_COLOR)
elif self._topic[CONF_HS_COMMAND_TOPIC] is not None:
self._hs = (0, 0)
@callback
def white_value_received(topic, payload, qos):
"""Handle new MQTT messages for white value."""
@ -403,7 +443,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
@callback
def xy_received(topic, payload, qos):
"""Handle new MQTT messages for color."""
"""Handle new MQTT messages for xy color."""
payload = templates[CONF_XY](payload)
if not payload:
_LOGGER.debug("Ignoring empty xy-color message from '%s'",
@ -539,6 +579,19 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light):
self._hs = kwargs[ATTR_HS_COLOR]
should_update = True
if ATTR_HS_COLOR in kwargs and \
self._topic[CONF_HS_COMMAND_TOPIC] is not None:
hs_color = kwargs[ATTR_HS_COLOR]
mqtt.async_publish(
self.hass, self._topic[CONF_HS_COMMAND_TOPIC],
'{},{}'.format(*hs_color), self._qos,
self._retain)
if self._optimistic_hs:
self._hs = kwargs[ATTR_HS_COLOR]
should_update = True
if ATTR_HS_COLOR in kwargs and \
self._topic[CONF_XY_COMMAND_TOPIC] is not None:

View File

@ -82,6 +82,7 @@ CONF_FRIENDLY_NAME_TEMPLATE = 'friendly_name_template'
CONF_HEADERS = 'headers'
CONF_HOST = 'host'
CONF_HOSTS = 'hosts'
CONF_HS = 'hs'
CONF_ICON = 'icon'
CONF_ICON_TEMPLATE = 'icon_template'
CONF_INCLUDE = 'include'

View File

@ -137,6 +137,21 @@ light:
payload_on: "on"
payload_off: "off"
Configuration for HS Version with brightness:
light:
platform: mqtt
name: "Office Light HS"
state_topic: "office/hs1/light/status"
command_topic: "office/hs1/light/switch"
brightness_state_topic: "office/hs1/brightness/status"
brightness_command_topic: "office/hs1/brightness/set"
hs_state_topic: "office/hs1/hs/status"
hs_command_topic: "office/hs1/hs/set"
qos: 0
payload_on: "on"
payload_off: "off"
"""
import unittest
from unittest import mock
@ -180,7 +195,7 @@ class TestLightMQTT(unittest.TestCase):
})
self.assertIsNone(self.hass.states.get('light.test'))
def test_no_color_brightness_color_temp_white_xy_if_no_topics(self):
def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(self):
"""Test if there is no color and brightness if no topic."""
with assert_setup_component(1, light.DOMAIN):
assert setup_component(self.hass, light.DOMAIN, {
@ -197,6 +212,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
@ -208,6 +224,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
@ -226,6 +243,8 @@ class TestLightMQTT(unittest.TestCase):
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_state_topic': 'test_light_rgb/effect/status',
'effect_command_topic': 'test_light_rgb/effect/set',
'hs_state_topic': 'test_light_rgb/hs/status',
'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_state_topic': 'test_light_rgb/white_value/status',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_state_topic': 'test_light_rgb/xy/status',
@ -244,6 +263,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('color_temp'))
self.assertIsNone(state.attributes.get('effect'))
self.assertIsNone(state.attributes.get('hs_color'))
self.assertIsNone(state.attributes.get('white_value'))
self.assertIsNone(state.attributes.get('xy_color'))
self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
@ -257,6 +277,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(255, state.attributes.get('brightness'))
self.assertEqual(150, state.attributes.get('color_temp'))
self.assertEqual('none', state.attributes.get('effect'))
self.assertEqual((0, 0), state.attributes.get('hs_color'))
self.assertEqual(255, state.attributes.get('white_value'))
self.assertEqual((0.323, 0.329), state.attributes.get('xy_color'))
@ -309,6 +330,14 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual((255, 255, 255),
light_state.attributes.get('rgb_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/hs/status',
'200,50')
self.hass.block_till_done()
light_state = self.hass.states.get('light.test')
self.assertEqual((200, 50),
light_state.attributes.get('hs_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
'0.675,0.322')
self.hass.block_till_done()
@ -412,7 +441,7 @@ class TestLightMQTT(unittest.TestCase):
light_state.attributes['white_value'])
def test_controlling_state_via_topic_with_templates(self):
"""Test the setting og the state with a template."""
"""Test the setting of the state with a template."""
config = {light.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
@ -422,11 +451,13 @@ class TestLightMQTT(unittest.TestCase):
'rgb_command_topic': 'test_light_rgb/rgb/set',
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_command_topic': 'test_light_rgb/effect/set',
'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_command_topic': 'test_light_rgb/xy/set',
'brightness_state_topic': 'test_light_rgb/brightness/status',
'color_temp_state_topic': 'test_light_rgb/color_temp/status',
'effect_state_topic': 'test_light_rgb/effect/status',
'hs_state_topic': 'test_light_rgb/hs/status',
'rgb_state_topic': 'test_light_rgb/rgb/status',
'white_value_state_topic': 'test_light_rgb/white_value/status',
'xy_state_topic': 'test_light_rgb/xy/status',
@ -434,6 +465,7 @@ class TestLightMQTT(unittest.TestCase):
'brightness_value_template': '{{ value_json.hello }}',
'color_temp_value_template': '{{ value_json.hello }}',
'effect_value_template': '{{ value_json.hello }}',
'hs_value_template': '{{ value_json.hello | join(",") }}',
'rgb_value_template': '{{ value_json.hello | join(",") }}',
'white_value_template': '{{ value_json.hello }}',
'xy_value_template': '{{ value_json.hello | join(",") }}',
@ -459,17 +491,28 @@ class TestLightMQTT(unittest.TestCase):
'{"hello": "rainbow"}')
fire_mqtt_message(self.hass, 'test_light_rgb/white_value/status',
'{"hello": "75"}')
fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
'{"hello": [0.123,0.123]}')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(50, state.attributes.get('brightness'))
self.assertEqual((0, 123, 255), state.attributes.get('rgb_color'))
self.assertEqual((84, 169, 255), state.attributes.get('rgb_color'))
self.assertEqual(300, state.attributes.get('color_temp'))
self.assertEqual('rainbow', state.attributes.get('effect'))
self.assertEqual(75, state.attributes.get('white_value'))
fire_mqtt_message(self.hass, 'test_light_rgb/hs/status',
'{"hello": [100,50]}')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual((100, 50), state.attributes.get('hs_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/xy/status',
'{"hello": [0.123,0.123]}')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual((0.14, 0.131), state.attributes.get('xy_color'))
def test_sending_mqtt_commands_and_optimistic(self):
@ -482,6 +525,7 @@ class TestLightMQTT(unittest.TestCase):
'rgb_command_topic': 'test_light_rgb/rgb/set',
'color_temp_command_topic': 'test_light_rgb/color_temp/set',
'effect_command_topic': 'test_light_rgb/effect/set',
'hs_command_topic': 'test_light_rgb/hs/set',
'white_value_command_topic': 'test_light_rgb/white_value/set',
'xy_command_topic': 'test_light_rgb/xy/set',
'effect_list': ['colorloop', 'random'],
@ -529,6 +573,8 @@ class TestLightMQTT(unittest.TestCase):
self.mock_publish.reset_mock()
common.turn_on(self.hass, 'light.test',
brightness=50, xy_color=[0.123, 0.123])
common.turn_on(self.hass, 'light.test',
brightness=50, hs_color=[359, 78])
common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0],
white_value=80)
self.hass.block_till_done()
@ -537,6 +583,7 @@ class TestLightMQTT(unittest.TestCase):
mock.call('test_light_rgb/set', 'on', 2, False),
mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False),
mock.call('test_light_rgb/brightness/set', 50, 2, False),
mock.call('test_light_rgb/hs/set', '359.0,78.0', 2, False),
mock.call('test_light_rgb/white_value/set', 80, 2, False),
mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False),
], any_order=True)
@ -545,6 +592,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(STATE_ON, state.state)
self.assertEqual((255, 128, 0), state.attributes['rgb_color'])
self.assertEqual(50, state.attributes['brightness'])
self.assertEqual((30.118, 100), state.attributes['hs_color'])
self.assertEqual(80, state.attributes['white_value'])
self.assertEqual((0.611, 0.375), state.attributes['xy_color'])
@ -652,6 +700,30 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual(STATE_ON, state.state)
self.assertEqual('none', state.attributes.get('effect'))
def test_show_hs_if_only_command_topic(self):
"""Test the hs if only a command topic is present."""
config = {light.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'hs_command_topic': 'test_light_rgb/hs/set',
'command_topic': 'test_light_rgb/set',
'state_topic': 'test_light_rgb/status',
}}
with assert_setup_component(1, light.DOMAIN):
assert setup_component(self.hass, light.DOMAIN, config)
state = self.hass.states.get('light.test')
self.assertEqual(STATE_OFF, state.state)
self.assertIsNone(state.attributes.get('hs_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'ON')
self.hass.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual((0, 0), state.attributes.get('hs_color'))
def test_show_white_value_if_only_command_topic(self):
"""Test the white_value if only a command topic is present."""
config = {light.DOMAIN: {