Skip to content

Commit eea22d8

Browse files
Add config flow for datadog (home-assistant#148104)
Co-authored-by: G Johansson <[email protected]>
1 parent 393087c commit eea22d8

File tree

10 files changed

+671
-59
lines changed

10 files changed

+671
-59
lines changed

homeassistant/components/datadog/__init__.py

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import logging
44

5-
from datadog import initialize, statsd
5+
from datadog import DogStatsd, initialize
66
import voluptuous as vol
77

8+
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
89
from homeassistant.const import (
910
CONF_HOST,
1011
CONF_PORT,
@@ -17,14 +18,19 @@
1718
from homeassistant.helpers import config_validation as cv, state as state_helper
1819
from homeassistant.helpers.typing import ConfigType
1920

21+
from . import config_flow as config_flow
22+
from .const import (
23+
CONF_RATE,
24+
DEFAULT_HOST,
25+
DEFAULT_PORT,
26+
DEFAULT_PREFIX,
27+
DEFAULT_RATE,
28+
DOMAIN,
29+
)
30+
2031
_LOGGER = logging.getLogger(__name__)
2132

22-
CONF_RATE = "rate"
23-
DEFAULT_HOST = "localhost"
24-
DEFAULT_PORT = 8125
25-
DEFAULT_PREFIX = "hass"
26-
DEFAULT_RATE = 1
27-
DOMAIN = "datadog"
33+
type DatadogConfigEntry = ConfigEntry[DogStatsd]
2834

2935
CONFIG_SCHEMA = vol.Schema(
3036
{
@@ -43,63 +49,85 @@
4349
)
4450

4551

46-
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
47-
"""Set up the Datadog component."""
52+
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
53+
"""Set up the Datadog integration from YAML, initiating config flow import."""
54+
if DOMAIN not in config:
55+
return True
56+
57+
hass.async_create_task(
58+
hass.config_entries.flow.async_init(
59+
DOMAIN,
60+
context={"source": SOURCE_IMPORT},
61+
data=config[DOMAIN],
62+
)
63+
)
64+
65+
return True
66+
67+
68+
async def async_setup_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> bool:
69+
"""Set up Datadog from a config entry."""
4870

49-
conf = config[DOMAIN]
50-
host = conf[CONF_HOST]
51-
port = conf[CONF_PORT]
52-
sample_rate = conf[CONF_RATE]
53-
prefix = conf[CONF_PREFIX]
71+
data = entry.data
72+
options = entry.options
73+
host = data[CONF_HOST]
74+
port = data[CONF_PORT]
75+
prefix = options[CONF_PREFIX]
76+
sample_rate = options[CONF_RATE]
77+
78+
statsd_client = DogStatsd(host=host, port=port, namespace=prefix)
79+
entry.runtime_data = statsd_client
5480

5581
initialize(statsd_host=host, statsd_port=port)
5682

5783
def logbook_entry_listener(event):
58-
"""Listen for logbook entries and send them as events."""
5984
name = event.data.get("name")
6085
message = event.data.get("message")
6186

62-
statsd.event(
87+
entry.runtime_data.event(
6388
title="Home Assistant",
64-
text=f"%%% \n **{name}** {message} \n %%%",
89+
message=f"%%% \n **{name}** {message} \n %%%",
6590
tags=[
6691
f"entity:{event.data.get('entity_id')}",
6792
f"domain:{event.data.get('domain')}",
6893
],
6994
)
7095

71-
_LOGGER.debug("Sent event %s", event.data.get("entity_id"))
72-
7396
def state_changed_listener(event):
74-
"""Listen for new messages on the bus and sends them to Datadog."""
7597
state = event.data.get("new_state")
76-
7798
if state is None or state.state == STATE_UNKNOWN:
7899
return
79100

80-
states = dict(state.attributes)
81101
metric = f"{prefix}.{state.domain}"
82102
tags = [f"entity:{state.entity_id}"]
83103

84-
for key, value in states.items():
85-
if isinstance(value, (float, int)):
86-
attribute = f"{metric}.{key.replace(' ', '_')}"
104+
for key, value in state.attributes.items():
105+
if isinstance(value, (float, int, bool)):
87106
value = int(value) if isinstance(value, bool) else value
88-
statsd.gauge(attribute, value, sample_rate=sample_rate, tags=tags)
89-
90-
_LOGGER.debug("Sent metric %s: %s (tags: %s)", attribute, value, tags)
107+
attribute = f"{metric}.{key.replace(' ', '_')}"
108+
entry.runtime_data.gauge(
109+
attribute, value, sample_rate=sample_rate, tags=tags
110+
)
91111

92112
try:
93113
value = state_helper.state_as_number(state)
114+
entry.runtime_data.gauge(metric, value, sample_rate=sample_rate, tags=tags)
94115
except ValueError:
95-
_LOGGER.debug("Error sending %s: %s (tags: %s)", metric, state.state, tags)
96-
return
116+
pass
97117

98-
statsd.gauge(metric, value, sample_rate=sample_rate, tags=tags)
118+
entry.async_on_unload(
119+
hass.bus.async_listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener)
120+
)
121+
entry.async_on_unload(
122+
hass.bus.async_listen(EVENT_STATE_CHANGED, state_changed_listener)
123+
)
99124

100-
_LOGGER.debug("Sent metric %s: %s (tags: %s)", metric, value, tags)
125+
return True
101126

102-
hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener)
103-
hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener)
104127

128+
async def async_unload_entry(hass: HomeAssistant, entry: DatadogConfigEntry) -> bool:
129+
"""Unload a Datadog config entry."""
130+
runtime = entry.runtime_data
131+
runtime.flush()
132+
runtime.close_socket()
105133
return True
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Config flow for Datadog."""
2+
3+
from typing import Any
4+
5+
from datadog import DogStatsd
6+
import voluptuous as vol
7+
8+
from homeassistant.config_entries import (
9+
ConfigEntry,
10+
ConfigFlow,
11+
ConfigFlowResult,
12+
OptionsFlow,
13+
)
14+
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX
15+
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
16+
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
17+
18+
from .const import (
19+
CONF_RATE,
20+
DEFAULT_HOST,
21+
DEFAULT_PORT,
22+
DEFAULT_PREFIX,
23+
DEFAULT_RATE,
24+
DOMAIN,
25+
)
26+
27+
28+
class DatadogConfigFlow(ConfigFlow, domain=DOMAIN):
29+
"""Handle a config flow for Datadog."""
30+
31+
VERSION = 1
32+
33+
async def async_step_user(
34+
self, user_input: dict[str, Any] | None = None
35+
) -> ConfigFlowResult:
36+
"""Handle user config flow."""
37+
errors: dict[str, str] = {}
38+
if user_input:
39+
# Validate connection to Datadog Agent
40+
success = await validate_datadog_connection(
41+
self.hass,
42+
user_input,
43+
)
44+
self._async_abort_entries_match(
45+
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
46+
)
47+
if not success:
48+
errors["base"] = "cannot_connect"
49+
else:
50+
return self.async_create_entry(
51+
title=f"Datadog {user_input['host']}",
52+
data={
53+
CONF_HOST: user_input[CONF_HOST],
54+
CONF_PORT: user_input[CONF_PORT],
55+
},
56+
options={
57+
CONF_PREFIX: user_input[CONF_PREFIX],
58+
CONF_RATE: user_input[CONF_RATE],
59+
},
60+
)
61+
62+
return self.async_show_form(
63+
step_id="user",
64+
data_schema=vol.Schema(
65+
{
66+
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
67+
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
68+
vol.Required(CONF_PREFIX, default=DEFAULT_PREFIX): str,
69+
vol.Required(CONF_RATE, default=DEFAULT_RATE): int,
70+
}
71+
),
72+
errors=errors,
73+
)
74+
75+
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
76+
"""Handle import from configuration.yaml."""
77+
# Check for duplicates
78+
self._async_abort_entries_match(
79+
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
80+
)
81+
82+
result = await self.async_step_user(user_input)
83+
84+
if errors := result.get("errors"):
85+
await deprecate_yaml_issue(self.hass, False)
86+
return self.async_abort(reason=errors["base"])
87+
88+
await deprecate_yaml_issue(self.hass, True)
89+
return result
90+
91+
@staticmethod
92+
@callback
93+
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
94+
"""Get the options flow handler."""
95+
return DatadogOptionsFlowHandler()
96+
97+
98+
class DatadogOptionsFlowHandler(OptionsFlow):
99+
"""Handle Datadog options."""
100+
101+
async def async_step_init(
102+
self, user_input: dict[str, Any] | None = None
103+
) -> ConfigFlowResult:
104+
"""Manage the Datadog options."""
105+
errors: dict[str, str] = {}
106+
data = self.config_entry.data
107+
options = self.config_entry.options
108+
109+
if user_input is None:
110+
user_input = {}
111+
112+
success = await validate_datadog_connection(
113+
self.hass,
114+
{**data, **user_input},
115+
)
116+
if success:
117+
return self.async_create_entry(
118+
data={
119+
CONF_PREFIX: user_input[CONF_PREFIX],
120+
CONF_RATE: user_input[CONF_RATE],
121+
}
122+
)
123+
124+
errors["base"] = "cannot_connect"
125+
return self.async_show_form(
126+
step_id="init",
127+
data_schema=vol.Schema(
128+
{
129+
vol.Required(CONF_PREFIX, default=options[CONF_PREFIX]): str,
130+
vol.Required(CONF_RATE, default=options[CONF_RATE]): int,
131+
}
132+
),
133+
errors=errors,
134+
)
135+
136+
137+
async def validate_datadog_connection(
138+
hass: HomeAssistant, user_input: dict[str, Any]
139+
) -> bool:
140+
"""Attempt to send a test metric to the Datadog agent."""
141+
try:
142+
client = DogStatsd(user_input[CONF_HOST], user_input[CONF_PORT])
143+
await hass.async_add_executor_job(client.increment, "connection_test")
144+
except (OSError, ValueError):
145+
return False
146+
else:
147+
return True
148+
149+
150+
async def deprecate_yaml_issue(
151+
hass: HomeAssistant,
152+
import_success: bool,
153+
) -> None:
154+
"""Create an issue to deprecate YAML config."""
155+
if import_success:
156+
async_create_issue(
157+
hass,
158+
HOMEASSISTANT_DOMAIN,
159+
f"deprecated_yaml_{DOMAIN}",
160+
is_fixable=False,
161+
issue_domain=DOMAIN,
162+
breaks_in_ha_version="2026.2.0",
163+
severity=IssueSeverity.WARNING,
164+
translation_key="deprecated_yaml",
165+
translation_placeholders={
166+
"domain": DOMAIN,
167+
"integration_title": "Datadog",
168+
},
169+
)
170+
else:
171+
async_create_issue(
172+
hass,
173+
DOMAIN,
174+
"deprecated_yaml_import_connection_error",
175+
breaks_in_ha_version="2026.2.0",
176+
is_fixable=False,
177+
issue_domain=DOMAIN,
178+
severity=IssueSeverity.WARNING,
179+
translation_key="deprecated_yaml_import_connection_error",
180+
translation_placeholders={
181+
"domain": DOMAIN,
182+
"integration_title": "Datadog",
183+
"url": f"/config/integrations/dashboard/add?domain={DOMAIN}",
184+
},
185+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Constants for the Datadog integration."""
2+
3+
DOMAIN = "datadog"
4+
5+
CONF_RATE = "rate"
6+
7+
DEFAULT_HOST = "localhost"
8+
DEFAULT_PORT = 8125
9+
DEFAULT_PREFIX = "hass"
10+
DEFAULT_RATE = 1

homeassistant/components/datadog/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"domain": "datadog",
33
"name": "Datadog",
44
"codeowners": [],
5+
"config_flow": true,
56
"documentation": "https://www.home-assistant.io/integrations/datadog",
67
"iot_class": "local_push",
78
"loggers": ["datadog"],

0 commit comments

Comments
 (0)