2019-02-14 05:35:12 +01:00
|
|
|
"""Support for Prometheus metrics export."""
|
2017-07-10 21:20:17 -07:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from aiohttp import web
|
2019-02-14 05:35:12 +01:00
|
|
|
import voluptuous as vol
|
2017-07-10 21:20:17 -07:00
|
|
|
|
2019-02-14 05:35:12 +01:00
|
|
|
from homeassistant import core as hacore
|
2019-02-14 20:34:43 +01:00
|
|
|
from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE
|
2017-07-10 21:20:17 -07:00
|
|
|
from homeassistant.components.http import HomeAssistantView
|
2017-08-25 13:30:00 +02:00
|
|
|
from homeassistant.const import (
|
2019-07-31 12:25:30 -07:00
|
|
|
ATTR_TEMPERATURE,
|
|
|
|
ATTR_UNIT_OF_MEASUREMENT,
|
|
|
|
CONTENT_TYPE_TEXT_PLAIN,
|
|
|
|
EVENT_STATE_CHANGED,
|
|
|
|
TEMP_FAHRENHEIT,
|
|
|
|
)
|
2018-06-28 16:49:33 +02:00
|
|
|
from homeassistant.helpers import entityfilter, state as state_helper
|
2019-02-14 05:35:12 +01:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2017-07-17 02:25:20 -07:00
|
|
|
from homeassistant.util.temperature import fahrenheit_to_celsius
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
API_ENDPOINT = "/api/prometheus"
|
2017-07-10 21:20:17 -07:00
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
DOMAIN = "prometheus"
|
|
|
|
CONF_FILTER = "filter"
|
|
|
|
CONF_PROM_NAMESPACE = "namespace"
|
2018-06-28 16:49:33 +02:00
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
|
|
{
|
|
|
|
DOMAIN: vol.All(
|
|
|
|
{
|
|
|
|
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
|
|
|
vol.Optional(CONF_PROM_NAMESPACE): cv.string,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
extra=vol.ALLOW_EXTRA,
|
|
|
|
)
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
|
|
|
|
def setup(hass, config):
|
|
|
|
"""Activate Prometheus component."""
|
|
|
|
import prometheus_client
|
|
|
|
|
|
|
|
hass.http.register_view(PrometheusView(prometheus_client))
|
|
|
|
|
2018-06-28 16:49:33 +02:00
|
|
|
conf = config[DOMAIN]
|
|
|
|
entity_filter = conf[CONF_FILTER]
|
|
|
|
namespace = conf.get(CONF_PROM_NAMESPACE)
|
2018-08-22 00:17:29 -07:00
|
|
|
climate_units = hass.config.units.temperature_unit
|
2019-07-31 12:25:30 -07:00
|
|
|
metrics = PrometheusMetrics(
|
|
|
|
prometheus_client, entity_filter, namespace, climate_units
|
|
|
|
)
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event)
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-07-20 11:45:20 +03:00
|
|
|
class PrometheusMetrics:
|
2017-07-10 21:20:17 -07:00
|
|
|
"""Model all of the metrics which should be exposed to Prometheus."""
|
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
def __init__(self, prometheus_client, entity_filter, namespace, climate_units):
|
2017-07-10 21:20:17 -07:00
|
|
|
"""Initialize Prometheus Metrics."""
|
|
|
|
self.prometheus_client = prometheus_client
|
2018-06-28 16:49:33 +02:00
|
|
|
self._filter = entity_filter
|
|
|
|
if namespace:
|
|
|
|
self.metrics_prefix = "{}_".format(namespace)
|
|
|
|
else:
|
|
|
|
self.metrics_prefix = ""
|
2017-07-10 21:20:17 -07:00
|
|
|
self._metrics = {}
|
2018-08-22 00:17:29 -07:00
|
|
|
self._climate_units = climate_units
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
def handle_event(self, event):
|
|
|
|
"""Listen for new messages on the bus, and add them to Prometheus."""
|
2019-07-31 12:25:30 -07:00
|
|
|
state = event.data.get("new_state")
|
2017-07-10 21:20:17 -07:00
|
|
|
if state is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
entity_id = state.entity_id
|
|
|
|
_LOGGER.debug("Handling state update for %s", entity_id)
|
|
|
|
domain, _ = hacore.split_entity_id(entity_id)
|
|
|
|
|
2018-06-28 16:49:33 +02:00
|
|
|
if not self._filter(state.entity_id):
|
2017-07-10 21:20:17 -07:00
|
|
|
return
|
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
handler = "_handle_{}".format(domain)
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
if hasattr(self, handler):
|
|
|
|
getattr(self, handler)(state)
|
|
|
|
|
2018-05-05 09:23:01 -05:00
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"state_change",
|
2018-05-05 09:23:01 -05:00
|
|
|
self.prometheus_client.Counter,
|
2019-07-31 12:25:30 -07:00
|
|
|
"The number of state changes",
|
2018-05-05 09:23:01 -05:00
|
|
|
)
|
|
|
|
metric.labels(**self._labels(state)).inc()
|
|
|
|
|
2017-07-10 21:20:17 -07:00
|
|
|
def _metric(self, metric, factory, documentation, labels=None):
|
|
|
|
if labels is None:
|
2019-07-31 12:25:30 -07:00
|
|
|
labels = ["entity", "friendly_name", "domain"]
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
try:
|
|
|
|
return self._metrics[metric]
|
|
|
|
except KeyError:
|
2018-06-28 16:49:33 +02:00
|
|
|
full_metric_name = "{}{}".format(self.metrics_prefix, metric)
|
2019-07-31 12:25:30 -07:00
|
|
|
self._metrics[metric] = factory(full_metric_name, documentation, labels)
|
2017-07-10 21:20:17 -07:00
|
|
|
return self._metrics[metric]
|
|
|
|
|
2019-03-22 13:16:17 -07:00
|
|
|
@staticmethod
|
|
|
|
def state_as_number(state):
|
|
|
|
"""Return a state casted to a float."""
|
|
|
|
try:
|
|
|
|
value = state_helper.state_as_number(state)
|
|
|
|
except ValueError:
|
|
|
|
_LOGGER.warning("Could not convert %s to float", state)
|
|
|
|
value = 0
|
|
|
|
return value
|
|
|
|
|
2017-07-10 21:20:17 -07:00
|
|
|
@staticmethod
|
|
|
|
def _labels(state):
|
|
|
|
return {
|
2019-07-31 12:25:30 -07:00
|
|
|
"entity": state.entity_id,
|
|
|
|
"domain": state.domain,
|
|
|
|
"friendly_name": state.attributes.get("friendly_name"),
|
2017-07-10 21:20:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
def _battery(self, state):
|
2019-07-31 12:25:30 -07:00
|
|
|
if "battery_level" in state.attributes:
|
2017-07-10 21:20:17 -07:00
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"battery_level_percent",
|
2017-07-10 21:20:17 -07:00
|
|
|
self.prometheus_client.Gauge,
|
2019-07-31 12:25:30 -07:00
|
|
|
"Battery level as a percentage of its capacity",
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
|
|
|
try:
|
2019-07-31 12:25:30 -07:00
|
|
|
value = float(state.attributes["battery_level"])
|
2017-07-10 21:20:17 -07:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _handle_binary_sensor(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"binary_sensor_state",
|
2017-07-10 21:20:17 -07:00
|
|
|
self.prometheus_client.Gauge,
|
2019-07-31 12:25:30 -07:00
|
|
|
"State of the binary sensor (0/1)",
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-07-10 21:20:17 -07:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
|
2018-11-06 07:19:36 -05:00
|
|
|
def _handle_input_boolean(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"input_boolean_state",
|
2018-11-06 07:19:36 -05:00
|
|
|
self.prometheus_client.Gauge,
|
2019-07-31 12:25:30 -07:00
|
|
|
"State of the input boolean (0/1)",
|
2018-11-06 07:19:36 -05:00
|
|
|
)
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2018-11-06 07:19:36 -05:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
|
2017-07-10 21:20:17 -07:00
|
|
|
def _handle_device_tracker(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"device_tracker_state",
|
2017-07-10 21:20:17 -07:00
|
|
|
self.prometheus_client.Gauge,
|
2019-07-31 12:25:30 -07:00
|
|
|
"State of the device tracker (0/1)",
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-07-10 21:20:17 -07:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
2019-02-23 17:13:27 +00:00
|
|
|
|
|
|
|
def _handle_person(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"person_state", self.prometheus_client.Gauge, "State of the person (0/1)"
|
2019-02-23 17:13:27 +00:00
|
|
|
)
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2019-02-23 17:13:27 +00:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
def _handle_light(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"light_state", self.prometheus_client.Gauge, "Load level of a light (0..1)"
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
try:
|
2019-07-31 12:25:30 -07:00
|
|
|
if "brightness" in state.attributes:
|
|
|
|
value = state.attributes["brightness"] / 255.0
|
2017-07-10 21:20:17 -07:00
|
|
|
else:
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-07-10 21:20:17 -07:00
|
|
|
value = value * 100
|
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
def _handle_lock(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"lock_state", self.prometheus_client.Gauge, "State of the lock (0/1)"
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-07-10 21:20:17 -07:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
|
2017-12-04 12:39:26 +00:00
|
|
|
def _handle_climate(self, state):
|
|
|
|
temp = state.attributes.get(ATTR_TEMPERATURE)
|
|
|
|
if temp:
|
2018-08-22 00:17:29 -07:00
|
|
|
if self._climate_units == TEMP_FAHRENHEIT:
|
2017-12-04 12:39:26 +00:00
|
|
|
temp = fahrenheit_to_celsius(temp)
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"temperature_c",
|
|
|
|
self.prometheus_client.Gauge,
|
|
|
|
"Temperature in degrees Celsius",
|
|
|
|
)
|
2017-12-04 12:39:26 +00:00
|
|
|
metric.labels(**self._labels(state)).set(temp)
|
|
|
|
|
2018-07-02 17:03:46 -05:00
|
|
|
current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE)
|
|
|
|
if current_temp:
|
2018-08-22 00:17:29 -07:00
|
|
|
if self._climate_units == TEMP_FAHRENHEIT:
|
2018-07-02 17:03:46 -05:00
|
|
|
current_temp = fahrenheit_to_celsius(current_temp)
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"current_temperature_c",
|
|
|
|
self.prometheus_client.Gauge,
|
|
|
|
"Current Temperature in degrees Celsius",
|
|
|
|
)
|
2018-07-02 17:03:46 -05:00
|
|
|
metric.labels(**self._labels(state)).set(current_temp)
|
|
|
|
|
2017-12-04 12:39:26 +00:00
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"climate_state",
|
|
|
|
self.prometheus_client.Gauge,
|
|
|
|
"State of the thermostat (0/1)",
|
|
|
|
)
|
2017-12-04 12:39:26 +00:00
|
|
|
try:
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-12-04 12:39:26 +00:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
2017-07-10 21:20:17 -07:00
|
|
|
def _handle_sensor(self, state):
|
|
|
|
|
2017-12-04 12:39:26 +00:00
|
|
|
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
2018-01-08 17:11:45 +01:00
|
|
|
metric = state.entity_id.split(".")[1]
|
2017-07-10 21:20:17 -07:00
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
if "_" not in str(metric):
|
|
|
|
metric = state.entity_id.replace(".", "_")
|
2018-04-10 08:20:47 +02:00
|
|
|
|
2018-01-08 17:11:45 +01:00
|
|
|
try:
|
|
|
|
int(metric.split("_")[-1])
|
|
|
|
metric = "_".join(metric.split("_")[:-1])
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
2019-07-31 12:25:30 -07:00
|
|
|
_metric = self._metric(metric, self.prometheus_client.Gauge, state.entity_id)
|
2018-01-08 17:11:45 +01:00
|
|
|
|
|
|
|
try:
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2018-01-08 17:11:45 +01:00
|
|
|
if unit == TEMP_FAHRENHEIT:
|
|
|
|
value = fahrenheit_to_celsius(value)
|
|
|
|
_metric.labels(**self._labels(state)).set(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
self._battery(state)
|
|
|
|
|
|
|
|
def _handle_switch(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)"
|
2017-07-10 21:20:17 -07:00
|
|
|
)
|
2017-12-03 16:39:54 -06:00
|
|
|
|
|
|
|
try:
|
2019-03-22 13:16:17 -07:00
|
|
|
value = self.state_as_number(state)
|
2017-12-03 16:39:54 -06:00
|
|
|
metric.labels(**self._labels(state)).set(value)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2017-07-10 21:20:17 -07:00
|
|
|
|
2017-07-23 23:49:03 -07:00
|
|
|
def _handle_zwave(self, state):
|
|
|
|
self._battery(state)
|
|
|
|
|
2017-12-03 16:39:54 -06:00
|
|
|
def _handle_automation(self, state):
|
|
|
|
metric = self._metric(
|
2019-07-31 12:25:30 -07:00
|
|
|
"automation_triggered_count",
|
2017-12-03 16:39:54 -06:00
|
|
|
self.prometheus_client.Counter,
|
2019-07-31 12:25:30 -07:00
|
|
|
"Count of times an automation has been triggered",
|
2017-12-03 16:39:54 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
metric.labels(**self._labels(state)).inc()
|
|
|
|
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
class PrometheusView(HomeAssistantView):
|
|
|
|
"""Handle Prometheus requests."""
|
|
|
|
|
|
|
|
url = API_ENDPOINT
|
2019-07-31 12:25:30 -07:00
|
|
|
name = "api:prometheus"
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
def __init__(self, prometheus_client):
|
|
|
|
"""Initialize Prometheus view."""
|
|
|
|
self.prometheus_client = prometheus_client
|
|
|
|
|
2018-10-01 08:52:42 +02:00
|
|
|
async def get(self, request):
|
2017-07-10 21:20:17 -07:00
|
|
|
"""Handle request for Prometheus metrics."""
|
2017-08-25 13:30:00 +02:00
|
|
|
_LOGGER.debug("Received Prometheus metrics request")
|
2017-07-10 21:20:17 -07:00
|
|
|
|
|
|
|
return web.Response(
|
|
|
|
body=self.prometheus_client.generate_latest(),
|
2019-07-31 12:25:30 -07:00
|
|
|
content_type=CONTENT_TYPE_TEXT_PLAIN,
|
|
|
|
)
|