core/homeassistant/components/prometheus/__init__.py

293 lines
9.3 KiB
Python
Raw Normal View History

"""Support for Prometheus metrics export."""
import logging
from aiohttp import web
import voluptuous as vol
from homeassistant import core as hacore
from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN,
EVENT_STATE_CHANGED, TEMP_FAHRENHEIT)
from homeassistant.helpers import entityfilter, state as state_helper
import homeassistant.helpers.config_validation as cv
from homeassistant.util.temperature import fahrenheit_to_celsius
_LOGGER = logging.getLogger(__name__)
API_ENDPOINT = '/api/prometheus'
DOMAIN = 'prometheus'
CONF_FILTER = 'filter'
CONF_PROM_NAMESPACE = 'namespace'
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)
def setup(hass, config):
"""Activate Prometheus component."""
import prometheus_client
hass.http.register_view(PrometheusView(prometheus_client))
conf = config[DOMAIN]
entity_filter = conf[CONF_FILTER]
namespace = conf.get(CONF_PROM_NAMESPACE)
climate_units = hass.config.units.temperature_unit
metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace,
climate_units)
hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event)
return True
class PrometheusMetrics:
"""Model all of the metrics which should be exposed to Prometheus."""
def __init__(self, prometheus_client, entity_filter, namespace,
climate_units):
"""Initialize Prometheus Metrics."""
self.prometheus_client = prometheus_client
self._filter = entity_filter
if namespace:
self.metrics_prefix = "{}_".format(namespace)
else:
self.metrics_prefix = ""
self._metrics = {}
self._climate_units = climate_units
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)
if not self._filter(state.entity_id):
return
handler = '_handle_{}'.format(domain)
if hasattr(self, handler):
getattr(self, handler)(state)
metric = self._metric(
'state_change',
self.prometheus_client.Counter,
'The number of state changes',
)
metric.labels(**self._labels(state)).inc()
def _metric(self, metric, factory, documentation, labels=None):
if labels is None:
labels = ['entity', 'friendly_name', 'domain']
try:
return self._metrics[metric]
except KeyError:
full_metric_name = "{}{}".format(self.metrics_prefix, metric)
self._metrics[metric] = factory(
full_metric_name, documentation, labels)
return self._metrics[metric]
@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
@staticmethod
def _labels(state):
return {
'entity': state.entity_id,
'domain': state.domain,
'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 = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_input_boolean(self, state):
metric = self._metric(
'input_boolean_state',
self.prometheus_client.Gauge,
'State of the input boolean (0/1)',
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_device_tracker(self, state):
metric = self._metric(
'device_tracker_state',
self.prometheus_client.Gauge,
'State of the device tracker (0/1)',
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_person(self, state):
metric = self._metric(
'person_state',
self.prometheus_client.Gauge,
'State of the person (0/1)',
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
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 = self.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 = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
def _handle_climate(self, state):
temp = state.attributes.get(ATTR_TEMPERATURE)
if temp:
if self._climate_units == TEMP_FAHRENHEIT:
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)
current_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if current_temp:
if self._climate_units == TEMP_FAHRENHEIT:
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)
metric = self._metric(
'climate_state', self.prometheus_client.Gauge,
'State of the thermostat (0/1)')
try:
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
def _handle_sensor(self, state):
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
metric = state.entity_id.split(".")[1]
if '_' not in str(metric):
metric = state.entity_id.replace('.', '_')
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 = self.state_as_number(state)
if unit == TEMP_FAHRENHEIT:
value = fahrenheit_to_celsius(value)
_metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
self._battery(state)
def _handle_switch(self, state):
metric = self._metric(
'switch_state',
self.prometheus_client.Gauge,
'State of the switch (0/1)',
)
try:
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
def _handle_zwave(self, state):
self._battery(state)
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()
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
async def get(self, request):
"""Handle request for Prometheus metrics."""
_LOGGER.debug("Received Prometheus metrics request")
return web.Response(
body=self.prometheus_client.generate_latest(),
content_type=CONTENT_TYPE_TEXT_PLAIN)