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