diff --git a/homeassistant/components/datadog.py b/homeassistant/components/datadog.py new file mode 100644 index 00000000000000..2c8145177b77ff --- /dev/null +++ b/homeassistant/components/datadog.py @@ -0,0 +1,120 @@ +""" +A component which allows you to send data to Datadog. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/datadog/ +""" +import logging +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, + STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['datadog==0.15.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RATE = 'rate' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8125 +DEFAULT_PREFIX = 'hass' +DEFAULT_RATE = 1 +DOMAIN = 'datadog' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + vol.Optional(CONF_RATE, default=DEFAULT_RATE): + vol.All(vol.Coerce(int), vol.Range(min=1)), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the Datadog component.""" + from datadog import initialize, statsd + + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + sample_rate = conf.get(CONF_RATE) + prefix = conf.get(CONF_PREFIX) + + initialize(statsd_host=host, statsd_port=port) + + def logbook_entry_listener(event): + """Listen for logbook entries and send them as events.""" + name = event.data.get('name') + message = event.data.get('message') + + statsd.event( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format(name, message), + tags=[ + "entity:{}".format(event.data.get('entity_id')), + "domain:{}".format(event.data.get('domain')) + ] + ) + + _LOGGER.debug('Sent event %s', event.data.get('entity_id')) + + def state_changed_listener(event): + """Listen for new messages on the bus and sends them to Datadog.""" + state = event.data.get('new_state') + + if state is None or state.state == STATE_UNKNOWN: + return + + if state.attributes.get('hidden') is True: + return + + states = dict(state.attributes) + metric = "{}.{}".format(prefix, state.domain) + tags = ["entity:{}".format(state.entity_id)] + + for key, value in states.items(): + if isinstance(value, (float, int)): + attribute = "{}.{}".format(metric, key.replace(' ', '_')) + statsd.gauge( + attribute, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug( + 'Sent metric %s: %s (tags: %s)', + attribute, + value, + tags + ) + + try: + value = state_helper.state_as_number(state) + except ValueError: + _LOGGER.debug( + 'Error sending %s: %s (tags: %s)', + metric, + state.state, + tags + ) + return + + statsd.gauge( + metric, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) + + hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) + hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) + + return True diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 98a0973a8071a5..bdc3fa3dce3c6e 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -19,7 +19,8 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST) + STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, + EVENT_LOGBOOK_ENTRY) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN DOMAIN = 'logbook' @@ -47,8 +48,6 @@ }), }, extra=vol.ALLOW_EXTRA) -EVENT_LOGBOOK_ENTRY = 'logbook_entry' - GROUP_BY_MINUTES = 15 ATTR_NAME = 'name' diff --git a/homeassistant/const.py b/homeassistant/const.py index c0ec2202a25533..5b2367db718199 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -172,6 +172,7 @@ EVENT_COMPONENT_LOADED = 'component_loaded' EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' # #### STATES #### STATE_ON = 'on' diff --git a/requirements_all.txt b/requirements_all.txt index ffa31684a412e6..64613fad7f0289 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,6 +124,9 @@ concord232==0.14 # homeassistant.components.sensor.crimereports crimereports==1.0.0 +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.sensor.metoffice # homeassistant.components.weather.metoffice datapoint==0.4.3 diff --git a/tests/components/test_datadog.py b/tests/components/test_datadog.py new file mode 100644 index 00000000000000..7e051161fc33ae --- /dev/null +++ b/tests/components/test_datadog.py @@ -0,0 +1,179 @@ +"""The tests for the Datadog component.""" +from unittest import mock +import unittest + +from homeassistant.const import ( + EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_ON +) +from homeassistant.setup import setup_component +import homeassistant.components.datadog as datadog +import homeassistant.core as ha + +from tests.common import (assert_setup_component, get_test_home_assistant) + + +class TestDatadog(unittest.TestCase): + """Test the Datadog component.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_config(self): + """Test invalid configuration.""" + with assert_setup_component(0): + assert not setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host1': 'host1' + } + }) + + @mock.patch('datadog.initialize') + def test_datadog_setup_full(self, mock_connection): + """Test setup with all data.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': 123, + 'rate': 1, + 'prefix': 'foo', + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=123) + ) + + self.assertTrue(self.hass.bus.listen.called) + self.assertEqual(EVENT_LOGBOOK_ENTRY, + self.hass.bus.listen.call_args_list[0][0][0]) + self.assertEqual(EVENT_STATE_CHANGED, + self.hass.bus.listen.call_args_list[1][0][0]) + + @mock.patch('datadog.initialize') + def test_datadog_setup_defaults(self, mock_connection): + """Test setup with defaults.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': datadog.DEFAULT_PORT, + 'prefix': datadog.DEFAULT_PREFIX, + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=8125) + ) + self.assertTrue(self.hass.bus.listen.called) + + @mock.patch('datadog.statsd') + def test_logbook_entry(self, mock_client): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[0][0][1] + + event = { + 'domain': 'automation', + 'entity_id': 'sensor.foo.bar', + 'message': 'foo bar biz', + 'name': 'triggered something' + } + handler_method(mock.MagicMock(data=event)) + + self.assertEqual(mock_client.event.call_count, 1) + self.assertEqual( + mock_client.event.call_args, + mock.call( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format( + event['name'], + event['message'] + ), + tags=["entity:sensor.foo.bar", "domain:automation"] + ) + ) + + mock_client.event.reset_mock() + + @mock.patch('datadog.statsd') + def test_state_changed(self, mock_client): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'prefix': 'ha', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[1][0][1] + + valid = { + '1': 1, + '1.0': 1.0, + STATE_ON: 1, + STATE_OFF: 0 + } + + attributes = { + 'elevation': 3.2, + 'temperature': 5.0 + } + + for in_, out in valid.items(): + state = mock.MagicMock(domain="sensor", entity_id="sensor.foo.bar", + state=in_, attributes=attributes) + handler_method(mock.MagicMock(data={'new_state': state})) + + self.assertEqual(mock_client.gauge.call_count, 3) + + for attribute, value in attributes.items(): + mock_client.gauge.assert_has_calls([ + mock.call( + "ha.sensor.{}".format(attribute), + value, + sample_rate=1, + tags=["entity:{}".format(state.entity_id)] + ) + ]) + + self.assertEqual( + mock_client.gauge.call_args, + mock.call("ha.sensor", out, sample_rate=1, tags=[ + "entity:{}".format(state.entity_id) + ]) + ) + + mock_client.gauge.reset_mock() + + for invalid in ('foo', '', object): + handler_method(mock.MagicMock(data={ + 'new_state': ha.State('domain.test', invalid, {})})) + self.assertFalse(mock_client.gauge.called)