diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 3b095aa4bfd..1c01278197d 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -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: diff --git a/homeassistant/const.py b/homeassistant/const.py index e9fb301f0db..361299181ac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -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' diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 118cdb3c995..2b23be101c7 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -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: {