From b1b0a2589e2de7c92a2d6f53fb3cffe70751ef81 Mon Sep 17 00:00:00 2001 From: timstanley1985 Date: Mon, 8 Jan 2018 16:07:39 +0000 Subject: [PATCH] MQTT json attributes (#11439) * MQTT json attributes * Fix lint * Amends following comments * Fix lint * Fix lint * Add test * Fix typo * Amends following comments * New tests * Fix lint * Fix tests --- homeassistant/components/sensor/mqtt.py | 28 +++++++- tests/components/sensor/test_mqtt.py | 87 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index f82c87c9ef5..b19f5721e4f 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.mqtt/ """ import asyncio import logging +import json from datetime import timedelta import voluptuous as vol @@ -26,6 +27,7 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = 'expire_after' +CONF_JSON_ATTRS = 'json_attributes' DEFAULT_NAME = 'MQTT Sensor' DEFAULT_FORCE_UPDATE = False @@ -34,6 +36,7 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -57,6 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), value_template, + config.get(CONF_JSON_ATTRS), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), @@ -68,7 +72,8 @@ class MqttSensor(MqttAvailability, Entity): def __init__(self, name, state_topic, qos, unit_of_measurement, force_update, expire_after, value_template, - availability_topic, payload_available, payload_not_available): + json_attributes, availability_topic, payload_available, + payload_not_available): """Initialize the sensor.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -81,6 +86,8 @@ class MqttSensor(MqttAvailability, Entity): self._template = value_template self._expire_after = expire_after self._expiration_trigger = None + self._json_attributes = set(json_attributes) + self._attributes = None @asyncio.coroutine def async_added_to_hass(self): @@ -104,6 +111,20 @@ class MqttSensor(MqttAvailability, Entity): self._expiration_trigger = async_track_point_in_utc_time( self.hass, self.value_is_expired, expiration_at) + if self._json_attributes: + self._attributes = {} + try: + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + attrs = {k: json_dict[k] for k in + self._json_attributes & json_dict.keys()} + self._attributes = attrs + else: + _LOGGER.warning("JSON result was not a dictionary") + except ValueError: + _LOGGER.warning("MQTT payload could not be parsed as JSON") + _LOGGER.debug("Erroneous JSON: %s", payload) + if self._template is not None: payload = self._template.async_render_with_possible_json_value( payload, self._state) @@ -144,3 +165,8 @@ class MqttSensor(MqttAvailability, Entity): def state(self): """Return the state of the entity.""" return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 4f9161a5b7f..d5cfad407d5 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -216,3 +216,90 @@ class TestSensorMQTT(unittest.TestCase): def _send_time_changed(self, now): """Send a time changed event.""" self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) + + def test_setting_sensor_attribute_via_mqtt_json_message(self): + """Test the setting of attribute via MQTT with JSON playload.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual('100', + state.attributes.get('val')) + + @patch('homeassistant.components.sensor.mqtt._LOGGER') + def test_update_with_json_attrs_not_dict(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '[ "list", "of", "things"]') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual(None, + state.attributes.get('val')) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.mqtt._LOGGER') + def test_update_with_json_attrs_bad_JSON(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', 'This is not JSON') + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual(None, + state.attributes.get('val')) + self.assertTrue(mock_logger.warning.called) + self.assertTrue(mock_logger.debug.called) + + def test_update_with_json_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + mock_component(self.hass, 'mqtt') + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'value_template': '{{ value_json.val }}', + 'json_attributes': 'val' + } + }) + + fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') + self.hass.block_till_done() + state = self.hass.states.get('sensor.test') + + self.assertEqual('100', + state.attributes.get('val')) + self.assertEqual('100', state.state)