From e99f23533a4a8ad2dc135f26a2651c33fc05b284 Mon Sep 17 00:00:00 2001 From: Matt White Date: Sun, 3 Sep 2017 20:11:53 -0600 Subject: [PATCH 1/6] Added mqtt_statestream component --- homeassistant/components/mqtt_statestream.py | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 homeassistant/components/mqtt_statestream.py diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py new file mode 100644 index 00000000000000..54f9a596a73545 --- /dev/null +++ b/homeassistant/components/mqtt_statestream.py @@ -0,0 +1,53 @@ +""" +Publish simple item state changes via MQTT. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mqtt_statestream/ +""" +import asyncio + +import voluptuous as vol + +import homeassistant.loader as loader +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import callback +from homeassistant.components.mqtt import valid_publish_topic + +CONF_BASE_TOPIC = 'base_topic' +DEPENDENCIES = ['mqtt'] +DOMAIN = 'mqtt_statestream' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_BASE_TOPIC): valid_publish_topic + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Set up the MQTT state feed.""" + mqtt = loader.get_component('mqtt') + conf = config.get(DOMAIN, {}) + base_topic = conf.get(CONF_BASE_TOPIC) + if not base_topic.endswith('/'): + base_topic = base_topic + '/' + + @callback + def _event_publisher(event): + """Handle state change events and publish them to MQTT.""" + if event.event_type == EVENT_STATE_CHANGED: + try: + new_state = event.data['new_state'] + payload = new_state.state + except NameError: + payload = None + + if payload is not None: + topic = base_topic + new_state.entity_id.replace('.', '/') + mqtt.async_publish(hass, topic, payload, 1, True) + + hass.bus.async_listen(EVENT_STATE_CHANGED, _event_publisher) + + hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) + return True From 730d6eab0484ded584b572640d26a0e571ea8b93 Mon Sep 17 00:00:00 2001 From: Matt White Date: Sun, 3 Sep 2017 20:55:22 -0600 Subject: [PATCH 2/6] Added tests for mqtt_statestream component --- tests/components/test_mqtt_statestream.py | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/components/test_mqtt_statestream.py diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py new file mode 100644 index 00000000000000..73e2dbd1ac4be9 --- /dev/null +++ b/tests/components/test_mqtt_statestream.py @@ -0,0 +1,65 @@ +"""The tests for the MQTT statestream component.""" +from unittest.mock import patch + +from homeassistant.setup import setup_component +import homeassistant.components.mqtt_statestream as statestream +from homeassistant.core import State + +from tests.common import ( + get_test_home_assistant, + mock_mqtt_component, + mock_state_change_event +) + + +class TestMqttStateStream(object): + """Test the MQTT statestream module.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_mqtt = mock_mqtt_component(self.hass) + + def teardown_method(self): + """Stop everything that was started.""" + self.hass.stop() + + def add_statestream(self, base_topic=None): + """Add a mqtt_statestream component.""" + config = {} + if base_topic: + config['base_topic'] = base_topic + return setup_component(self.hass, statestream.DOMAIN, { + statestream.DOMAIN: config}) + + def test_fails_with_no_base(self): + """Setup should fail if no base_topic is set.""" + assert self.add_statestream() is False + + def test_setup_succeeds(self): + """"Test the success of the setup with a valid base_topic.""" + assert self.add_statestream(base_topic='pub') + + @patch('homeassistant.components.mqtt.async_publish') + @patch('homeassistant.core.dt_util.utcnow') + def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): + """"Test the sending of a new message if event changed.""" + e_id = 'fake.entity' + base_topic = 'pub' + + # Add the statestream component for publishing state updates + assert self.add_statestream(base_topic=base_topic) + self.hass.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_statestream state change on initialization, etc. + mock_pub.reset_mock() + + # Set a state of an entity + mock_state_change_event(self.hass, State(e_id, 'on')) + self.hass.block_till_done() + + # Make sure 'on' was published to pub/fake/entity + mock_pub.assert_called_with(self.hass, 'pub/fake/entity', 'on', 1, + True) + assert mock_pub.called From f87f4922a3f5d763c09a5e233665fd447c3341cb Mon Sep 17 00:00:00 2001 From: Matt White Date: Sun, 3 Sep 2017 22:00:59 -0600 Subject: [PATCH 3/6] mqtt_statestream: add test for valid new_state --- homeassistant/components/mqtt_statestream.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 54f9a596a73545..31caa6ce5c285f 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -39,13 +39,16 @@ def _event_publisher(event): if event.event_type == EVENT_STATE_CHANGED: try: new_state = event.data['new_state'] + except AttributeError: + return + + try: payload = new_state.state except NameError: - payload = None + return - if payload is not None: - topic = base_topic + new_state.entity_id.replace('.', '/') - mqtt.async_publish(hass, topic, payload, 1, True) + topic = base_topic + new_state.entity_id.replace('.', '/') + mqtt.async_publish(hass, topic, payload, 1, True) hass.bus.async_listen(EVENT_STATE_CHANGED, _event_publisher) From 9f75ff1affb08a9d99d990db338b7e9cf9bdbb17 Mon Sep 17 00:00:00 2001 From: Matt White Date: Mon, 4 Sep 2017 12:33:09 -0600 Subject: [PATCH 4/6] mqtt_statestream: Don't set initialized state --- homeassistant/components/mqtt_statestream.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 31caa6ce5c285f..819cf92be25886 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -52,5 +52,4 @@ def _event_publisher(event): hass.bus.async_listen(EVENT_STATE_CHANGED, _event_publisher) - hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) return True From a34a56304f9d4aae47ca4743344bdb54870b5b49 Mon Sep 17 00:00:00 2001 From: Matt White Date: Sat, 9 Sep 2017 14:35:27 -0600 Subject: [PATCH 5/6] mqtt_statestream: Switch to using async_track_state_change --- homeassistant/components/mqtt_statestream.py | 30 ++++++++------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 819cf92be25886..9da9062053ff79 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -8,10 +8,10 @@ import voluptuous as vol -import homeassistant.loader as loader -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import MATCH_ALL from homeassistant.core import callback from homeassistant.components.mqtt import valid_publish_topic +from homeassistant.helpers.event import async_track_state_change CONF_BASE_TOPIC = 'base_topic' DEPENDENCIES = ['mqtt'] @@ -27,29 +27,23 @@ @asyncio.coroutine def async_setup(hass, config): """Set up the MQTT state feed.""" - mqtt = loader.get_component('mqtt') conf = config.get(DOMAIN, {}) base_topic = conf.get(CONF_BASE_TOPIC) if not base_topic.endswith('/'): base_topic = base_topic + '/' @callback - def _event_publisher(event): - """Handle state change events and publish them to MQTT.""" - if event.event_type == EVENT_STATE_CHANGED: - try: - new_state = event.data['new_state'] - except AttributeError: - return + def _state_publisher(entity_id, old_state, new_state): + if new_state is None: + return - try: - payload = new_state.state - except NameError: - return + try: + payload = new_state.state + except KeyError: + return - topic = base_topic + new_state.entity_id.replace('.', '/') - mqtt.async_publish(hass, topic, payload, 1, True) - - hass.bus.async_listen(EVENT_STATE_CHANGED, _event_publisher) + topic = base_topic + entity_id.replace('.', '/') + hass.components.mqtt.async_publish(topic, payload, 1, True) + async_track_state_change(hass, MATCH_ALL, _state_publisher) return True From 1d9bab9d85d0a7a4f72a4c23c46321f63ee3b5d7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 11 Sep 2017 21:38:59 +0200 Subject: [PATCH 6/6] Cleanup --- homeassistant/components/mqtt_statestream.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 9da9062053ff79..76154e4ab587dd 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -36,11 +36,7 @@ def async_setup(hass, config): def _state_publisher(entity_id, old_state, new_state): if new_state is None: return - - try: - payload = new_state.state - except KeyError: - return + payload = new_state.state topic = base_topic + entity_id.replace('.', '/') hass.components.mqtt.async_publish(topic, payload, 1, True)