diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index fdfc1961db3..3be8de5c722 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -20,7 +20,8 @@ from homeassistant.const import ( CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -107,7 +108,8 @@ PLATFORM_SCHEMA_BASIC = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In(VALUES_ON_COMMAND_TYPE), vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_basic(hass, config, async_add_entities, @@ -120,8 +122,8 @@ async def async_setup_entity_basic(hass, config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, RestoreEntity): +class MqttLight(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT light.""" def __init__(self, config, discovery_hash): @@ -152,6 +154,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -166,6 +169,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_BASIC(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -467,6 +471,7 @@ class MqttLight(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 6c986cbf49f..1c32b0c5783 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -17,7 +17,8 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, Light) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_DEVICE, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) @@ -80,7 +81,8 @@ PLATFORM_SCHEMA_JSON = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, @@ -90,7 +92,7 @@ async def async_setup_entity_json(hass: HomeAssistantType, config: ConfigType, # pylint: disable=too-many-ancestors -class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, +class MqttLightJson(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT JSON light.""" @@ -115,6 +117,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -129,6 +132,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_JSON(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -297,6 +301,7 @@ class MqttLightJson(MqttAvailability, MqttDiscoveryUpdate, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 53423679050..7020550710b 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -18,7 +18,8 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, - MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, subscription) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity @@ -66,7 +67,8 @@ PLATFORM_SCHEMA_TEMPLATE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, -}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) async def async_setup_entity_template(hass, config, async_add_entities, @@ -76,8 +78,8 @@ async def async_setup_entity_template(hass, config, async_add_entities, # pylint: disable=too-many-ancestors -class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - Light, RestoreEntity): +class MqttTemplate(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, Light, RestoreEntity): """Representation of a MQTT Template light.""" def __init__(self, config, discovery_hash): @@ -102,6 +104,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, device_config = config.get(CONF_DEVICE) + MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) @@ -116,6 +119,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Handle updated discovery message.""" config = PLATFORM_SCHEMA_TEMPLATE(discovery_payload) self._setup_from_config(config) + await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self._subscribe_topics() self.async_schedule_update_ha_state() @@ -270,6 +274,7 @@ class MqttTemplate(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, """Unsubscribe when removed.""" self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) @property diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 951a9f04be9..a424263af8c 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -1070,6 +1070,106 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 691e34104e1..7621da724c9 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -536,6 +536,111 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'json', + 'name': 'test', + 'command_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "schema": "json",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "schema": "json",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f9946fc5b88..509f2ee5d36 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -485,6 +485,121 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert '100' == state.attributes.get('val') + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('light.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt', + 'schema': 'template', + 'name': 'test', + 'command_topic': 'test-topic', + 'command_on_template': 'on,{{ transition }}', + 'command_off_template': 'off,{{ transition|d }}', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('light.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "schema": "template",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "schema": "template",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '100' == state.attributes.get('val') + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert '75' == state.attributes.get('val') + + async def test_unique_id(hass): """Test unique id option only creates one light per unique_id.""" await async_mock_mqtt_component(hass)