Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add datadog component #7158

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions homeassistant/components/datadog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenError: EOF in multi-line statement

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
5 changes: 2 additions & 3 deletions homeassistant/components/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -47,8 +48,6 @@
}),
}, extra=vol.ALLOW_EXTRA)

EVENT_LOGBOOK_ENTRY = 'logbook_entry'

GROUP_BY_MINUTES = 15

ATTR_NAME = 'name'
Expand Down
1 change: 1 addition & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
179 changes: 179 additions & 0 deletions tests/components/test_datadog.py
Original file line number Diff line number Diff line change
@@ -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)