Add JSON attribute topic to MQTT binary sensor

Add MqttAttributes mixin
This commit is contained in:
Erik 2018-12-02 17:00:31 +01:00
parent b7e2522083
commit b9ad19acbf
3 changed files with 136 additions and 6 deletions

View file

@ -16,10 +16,10 @@ from homeassistant.const import (
CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE) CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS, CONF_DEVICE)
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
subscription) MqttEntityDeviceInfo, subscription)
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -49,7 +49,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
# This is an exception because MQTT is a message transport, not a protocol # This is an exception because MQTT is a message transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, 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_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
@ -76,7 +77,7 @@ async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
async_add_entities([MqttBinarySensor(config, discovery_hash)]) async_add_entities([MqttBinarySensor(config, discovery_hash)])
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo, BinarySensorDevice): MqttEntityDeviceInfo, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""
@ -94,6 +95,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
qos = config.get(CONF_QOS) qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE) device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
MqttAvailability.__init__(self, availability_topic, qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash, MqttDiscoveryUpdate.__init__(self, discovery_hash,
@ -109,6 +111,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
"""Handle updated discovery message.""" """Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload) config = PLATFORM_SCHEMA(discovery_payload)
self._config = config self._config = config
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config) await self.availability_discovery_update(config)
await self._subscribe_topics() await self._subscribe_topics()
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -164,6 +167,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
await subscription.async_unsubscribe_topics(self.hass, 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) await MqttAvailability.async_will_remove_from_hass(self)
@property @property

View file

@ -6,6 +6,7 @@ https://home-assistant.io/components/mqtt/
""" """
import asyncio import asyncio
from itertools import groupby from itertools import groupby
import json
import logging import logging
from operator import attrgetter from operator import attrgetter
import os import os
@ -70,6 +71,7 @@ CONF_COMMAND_TOPIC = 'command_topic'
CONF_AVAILABILITY_TOPIC = 'availability_topic' CONF_AVAILABILITY_TOPIC = 'availability_topic'
CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_AVAILABLE = 'payload_available'
CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available'
CONF_JSON_ATTRS_TOPIC = 'json_attributes_topic'
CONF_QOS = 'qos' CONF_QOS = 'qos'
CONF_RETAIN = 'retain' CONF_RETAIN = 'retain'
@ -224,6 +226,10 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({
vol.Optional(CONF_SW_VERSION): cv.string, vol.Optional(CONF_SW_VERSION): cv.string,
}), validate_device_has_at_least_one_identifier) }), validate_device_has_at_least_one_identifier)
MQTT_JSON_ATTRS_SCHEMA = vol.Schema({
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
})
MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE)
# Sensor type platforms subscribe to MQTT events # Sensor type platforms subscribe to MQTT events
@ -820,6 +826,65 @@ def _match_topic(subscription: str, topic: str) -> bool:
return False return False
class MqttAttributes(Entity):
"""Mixin used for platforms that support JSON attributes."""
def __init__(self, config: dict) -> None:
"""Initialize the JSON attributes mixin."""
self._attributes = None
self._attributes_sub_state = None
self._attributes_config = config
async def async_added_to_hass(self) -> None:
"""Subscribe MQTT events.
This method must be run in the event loop and returns a coroutine.
"""
await self._attributes_subscribe_topics()
async def attributes_discovery_update(self, config: dict):
"""Handle updated discovery message."""
self._attributes_config = config
await self._attributes_subscribe_topics()
async def _attributes_subscribe_topics(self):
"""(Re)Subscribe to topics."""
from .subscription import async_subscribe_topics
@callback
def attributes_message_received(topic: str,
payload: SubscribePayloadType,
qos: int) -> None:
try:
json_dict = json.loads(payload)
if isinstance(json_dict, dict):
self._attributes = json_dict
self.async_schedule_update_ha_state()
else:
_LOGGER.debug("JSON result was not a dictionary")
self._attributes = None
except ValueError:
_LOGGER.debug("Erroneous JSON: %s", payload)
self._attributes = None
self._attributes_sub_state = await async_subscribe_topics(
self.hass, self._attributes_sub_state,
{'attributes_topic': {
'topic': self._attributes_config.get(CONF_JSON_ATTRS_TOPIC),
'msg_callback': attributes_message_received,
'qos': self._attributes_config.get(CONF_QOS)}})
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
from .subscription import async_unsubscribe_topics
await async_unsubscribe_topics(self.hass, self._attributes_sub_state)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attributes
class MqttAvailability(Entity): class MqttAvailability(Entity):
"""Mixin used for platforms that report availability.""" """Mixin used for platforms that report availability."""

View file

@ -1,7 +1,7 @@
"""The tests for the MQTT binary sensor platform.""" """The tests for the MQTT binary sensor platform."""
import json import json
import unittest import unittest
from unittest.mock import Mock from unittest.mock import Mock, patch
from datetime import timedelta from datetime import timedelta
import homeassistant.core as ha import homeassistant.core as ha
@ -256,6 +256,67 @@ class TestSensorMQTT(unittest.TestCase):
assert STATE_OFF == state.state assert STATE_OFF == state.state
assert 3 == len(events) assert 3 == len(events)
def test_setting_sensor_attribute_via_mqtt_json_message(self):
"""Test the setting of attribute via MQTT with JSON payload."""
mock_component(self.hass, 'mqtt')
assert setup_component(self.hass, binary_sensor.DOMAIN, {
binary_sensor.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
fire_mqtt_message(self.hass, 'attr-topic', '{ "val": "100" }')
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
assert '100' == \
state.attributes.get('val')
@patch('homeassistant.components.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, binary_sensor.DOMAIN, {
binary_sensor.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
fire_mqtt_message(self.hass, 'attr-topic', '[ "list", "of", "things"]')
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
assert state.attributes.get('val') is None
mock_logger.debug.assert_called_with(
'JSON result was not a dictionary')
@patch('homeassistant.components.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, binary_sensor.DOMAIN, {
binary_sensor.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
fire_mqtt_message(self.hass, 'attr-topic', 'This is not JSON')
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test')
assert state.attributes.get('val') is None
mock_logger.debug.assert_called_with(
'Erroneous JSON: %s', 'This is not JSON')
async def test_unique_id(hass): async def test_unique_id(hass):
"""Test unique id option only creates one sensor per unique_id.""" """Test unique id option only creates one sensor per unique_id."""