Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Prometheus metrics export."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from contextlib import suppress
7 import logging
8 import string
9 from typing import Any, cast
10 
11 from aiohttp import web
12 import prometheus_client
13 from prometheus_client.metrics import MetricWrapperBase
14 import voluptuous as vol
15 
16 from homeassistant import core as hacore
17 from homeassistant.components.alarm_control_panel import AlarmControlPanelState
19  ATTR_CURRENT_TEMPERATURE,
20  ATTR_FAN_MODE,
21  ATTR_FAN_MODES,
22  ATTR_HVAC_ACTION,
23  ATTR_HVAC_MODES,
24  ATTR_TARGET_TEMP_HIGH,
25  ATTR_TARGET_TEMP_LOW,
26  HVACAction,
27 )
29  ATTR_CURRENT_POSITION,
30  ATTR_CURRENT_TILT_POSITION,
31 )
32 from homeassistant.components.fan import (
33  ATTR_DIRECTION,
34  ATTR_OSCILLATING,
35  ATTR_PERCENTAGE,
36  ATTR_PRESET_MODE,
37  ATTR_PRESET_MODES,
38  DIRECTION_FORWARD,
39  DIRECTION_REVERSE,
40 )
41 from homeassistant.components.http import KEY_HASS, HomeAssistantView
42 from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY
43 from homeassistant.components.light import ATTR_BRIGHTNESS
44 from homeassistant.components.sensor import SensorDeviceClass
45 from homeassistant.const import (
46  ATTR_BATTERY_LEVEL,
47  ATTR_DEVICE_CLASS,
48  ATTR_FRIENDLY_NAME,
49  ATTR_MODE,
50  ATTR_TEMPERATURE,
51  ATTR_UNIT_OF_MEASUREMENT,
52  CONTENT_TYPE_TEXT_PLAIN,
53  EVENT_STATE_CHANGED,
54  PERCENTAGE,
55  STATE_CLOSED,
56  STATE_CLOSING,
57  STATE_ON,
58  STATE_OPEN,
59  STATE_OPENING,
60  STATE_UNAVAILABLE,
61  STATE_UNKNOWN,
62  UnitOfTemperature,
63 )
64 from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State
65 from homeassistant.helpers import entityfilter, state as state_helper
68  EVENT_ENTITY_REGISTRY_UPDATED,
69  EventEntityRegistryUpdatedData,
70 )
71 from homeassistant.helpers.entity_values import EntityValues
72 from homeassistant.helpers.typing import ConfigType
73 from homeassistant.util.dt import as_timestamp
74 from homeassistant.util.unit_conversion import TemperatureConverter
75 
76 _LOGGER = logging.getLogger(__name__)
77 
78 API_ENDPOINT = "/api/prometheus"
79 IGNORED_STATES = frozenset({STATE_UNAVAILABLE, STATE_UNKNOWN})
80 
81 
82 DOMAIN = "prometheus"
83 CONF_FILTER = "filter"
84 CONF_REQUIRES_AUTH = "requires_auth"
85 CONF_PROM_NAMESPACE = "namespace"
86 CONF_COMPONENT_CONFIG = "component_config"
87 CONF_COMPONENT_CONFIG_GLOB = "component_config_glob"
88 CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain"
89 CONF_DEFAULT_METRIC = "default_metric"
90 CONF_OVERRIDE_METRIC = "override_metric"
91 COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema(
92  {vol.Optional(CONF_OVERRIDE_METRIC): cv.string}
93 )
94 ALLOWED_METRIC_CHARS = set(string.ascii_letters + string.digits + "_:")
95 
96 DEFAULT_NAMESPACE = "homeassistant"
97 
98 CONFIG_SCHEMA = vol.Schema(
99  {
100  DOMAIN: vol.All(
101  {
102  vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
103  vol.Optional(CONF_PROM_NAMESPACE, default=DEFAULT_NAMESPACE): cv.string,
104  vol.Optional(CONF_REQUIRES_AUTH, default=True): cv.boolean,
105  vol.Optional(CONF_DEFAULT_METRIC): cv.string,
106  vol.Optional(CONF_OVERRIDE_METRIC): cv.string,
107  vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema(
108  {cv.entity_id: COMPONENT_CONFIG_SCHEMA_ENTRY}
109  ),
110  vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): vol.Schema(
111  {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}
112  ),
113  vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema(
114  {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}
115  ),
116  }
117  )
118  },
119  extra=vol.ALLOW_EXTRA,
120 )
121 
122 
123 def setup(hass: HomeAssistant, config: ConfigType) -> bool:
124  """Activate Prometheus component."""
125  hass.http.register_view(PrometheusView(config[DOMAIN][CONF_REQUIRES_AUTH]))
126 
127  conf: dict[str, Any] = config[DOMAIN]
128  entity_filter: entityfilter.EntityFilter = conf[CONF_FILTER]
129  namespace: str = conf[CONF_PROM_NAMESPACE]
130  climate_units = hass.config.units.temperature_unit
131  override_metric: str | None = conf.get(CONF_OVERRIDE_METRIC)
132  default_metric: str | None = conf.get(CONF_DEFAULT_METRIC)
133  component_config = EntityValues(
134  conf[CONF_COMPONENT_CONFIG],
135  conf[CONF_COMPONENT_CONFIG_DOMAIN],
136  conf[CONF_COMPONENT_CONFIG_GLOB],
137  )
138 
139  metrics = PrometheusMetrics(
140  entity_filter,
141  namespace,
142  climate_units,
143  component_config,
144  override_metric,
145  default_metric,
146  )
147 
148  hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_state_changed_event)
149  hass.bus.listen(
150  EVENT_ENTITY_REGISTRY_UPDATED,
151  metrics.handle_entity_registry_updated,
152  )
153 
154  for state in hass.states.all():
155  if entity_filter(state.entity_id):
156  metrics.handle_state(state)
157 
158  return True
159 
160 
162  """Model all of the metrics which should be exposed to Prometheus."""
163 
164  def __init__(
165  self,
166  entity_filter: entityfilter.EntityFilter,
167  namespace: str,
168  climate_units: UnitOfTemperature,
169  component_config: EntityValues,
170  override_metric: str | None,
171  default_metric: str | None,
172  ) -> None:
173  """Initialize Prometheus Metrics."""
174  self._component_config_component_config = component_config
175  self._override_metric_override_metric = override_metric
176  self._default_metric_default_metric = default_metric
177  self._filter_filter = entity_filter
178  self._sensor_metric_handlers: list[
179  Callable[[State, str | None], str | None]
180  ] = [
181  self._sensor_override_component_metric_sensor_override_component_metric,
182  self._sensor_override_metric_sensor_override_metric,
183  self._sensor_timestamp_metric_sensor_timestamp_metric,
184  self._sensor_attribute_metric_sensor_attribute_metric,
185  self._sensor_default_metric_sensor_default_metric,
186  self._sensor_fallback_metric_sensor_fallback_metric,
187  ]
188 
189  if namespace:
190  self.metrics_prefixmetrics_prefix = f"{namespace}_"
191  else:
192  self.metrics_prefixmetrics_prefix = ""
193  self._metrics: dict[str, MetricWrapperBase] = {}
194  self._climate_units_climate_units = climate_units
195 
196  def handle_state_changed_event(self, event: Event[EventStateChangedData]) -> None:
197  """Handle new messages from the bus."""
198  if (state := event.data.get("new_state")) is None:
199  return
200 
201  if not self._filter_filter(state.entity_id):
202  _LOGGER.debug("Filtered out entity %s", state.entity_id)
203  return
204 
205  if (old_state := event.data.get("old_state")) is not None and (
206  old_friendly_name := old_state.attributes.get(ATTR_FRIENDLY_NAME)
207  ) != state.attributes.get(ATTR_FRIENDLY_NAME):
208  self._remove_labelsets_remove_labelsets(old_state.entity_id, old_friendly_name)
209 
210  self.handle_statehandle_state(state)
211 
212  def handle_state(self, state: State) -> None:
213  """Add/update a state in Prometheus."""
214  entity_id = state.entity_id
215  _LOGGER.debug("Handling state update for %s", entity_id)
216 
217  labels = self._labels_labels(state)
218  state_change = self._metric(
219  "state_change", prometheus_client.Counter, "The number of state changes"
220  )
221  state_change.labels(**labels).inc()
222 
223  entity_available = self._metric(
224  "entity_available",
225  prometheus_client.Gauge,
226  "Entity is available (not in the unavailable or unknown state)",
227  )
228  entity_available.labels(**labels).set(float(state.state not in IGNORED_STATES))
229 
230  last_updated_time_seconds = self._metric(
231  "last_updated_time_seconds",
232  prometheus_client.Gauge,
233  "The last_updated timestamp",
234  )
235  last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp())
236 
237  if state.state in IGNORED_STATES:
238  self._remove_labelsets_remove_labelsets(
239  entity_id,
240  None,
241  {state_change, entity_available, last_updated_time_seconds},
242  )
243  else:
244  domain, _ = hacore.split_entity_id(entity_id)
245  handler = f"_handle_{domain}"
246  if hasattr(self, handler) and state.state:
247  getattr(self, handler)(state)
248 
250  self, event: Event[EventEntityRegistryUpdatedData]
251  ) -> None:
252  """Listen for deleted, disabled or renamed entities and remove them from the Prometheus Registry."""
253  if event.data["action"] in (None, "create"):
254  return
255 
256  entity_id = event.data.get("entity_id")
257  _LOGGER.debug("Handling entity update for %s", entity_id)
258 
259  metrics_entity_id: str | None = None
260 
261  if event.data["action"] == "remove":
262  metrics_entity_id = entity_id
263  elif event.data["action"] == "update":
264  changes = event.data["changes"]
265 
266  if "entity_id" in changes:
267  metrics_entity_id = changes["entity_id"]
268  elif "disabled_by" in changes:
269  metrics_entity_id = entity_id
270 
271  if metrics_entity_id:
272  self._remove_labelsets_remove_labelsets(metrics_entity_id)
273 
275  self,
276  entity_id: str,
277  friendly_name: str | None = None,
278  ignored_metrics: set[MetricWrapperBase] | None = None,
279  ) -> None:
280  """Remove labelsets matching the given entity id from all non-ignored metrics."""
281  if ignored_metrics is None:
282  ignored_metrics = set()
283  for metric in list(self._metrics.values()):
284  if metric in ignored_metrics:
285  continue
286  for sample in cast(list[prometheus_client.Metric], metric.collect())[
287  0
288  ].samples:
289  if sample.labels["entity"] == entity_id and (
290  not friendly_name or sample.labels["friendly_name"] == friendly_name
291  ):
292  _LOGGER.debug(
293  "Removing labelset from %s for entity_id: %s",
294  sample.name,
295  entity_id,
296  )
297  with suppress(KeyError):
298  metric.remove(*sample.labels.values())
299 
300  def _handle_attributes(self, state: State) -> None:
301  for key, value in state.attributes.items():
302  metric = self._metric(
303  f"{state.domain}_attr_{key.lower()}",
304  prometheus_client.Gauge,
305  f"{key} attribute of {state.domain} entity",
306  )
307 
308  try:
309  value = float(value)
310  metric.labels(**self._labels_labels(state)).set(value)
311  except (ValueError, TypeError):
312  pass
313 
314  def _metric[_MetricBaseT: MetricWrapperBase](
315  self,
316  metric: str,
317  factory: type[_MetricBaseT],
318  documentation: str,
319  extra_labels: list[str] | None = None,
320  ) -> _MetricBaseT:
321  labels = ["entity", "friendly_name", "domain"]
322  if extra_labels is not None:
323  labels.extend(extra_labels)
324 
325  try:
326  return cast(_MetricBaseT, self._metrics[metric])
327  except KeyError:
328  full_metric_name = self._sanitize_metric_name_sanitize_metric_name(
329  f"{self.metrics_prefix}{metric}"
330  )
331  self._metrics[metric] = factory(
332  full_metric_name,
333  documentation,
334  labels,
335  registry=prometheus_client.REGISTRY,
336  )
337  return cast(_MetricBaseT, self._metrics[metric])
338 
339  @staticmethod
340  def _sanitize_metric_name(metric: str) -> str:
341  return "".join(
342  [c if c in ALLOWED_METRIC_CHARS else f"u{hex(ord(c))}" for c in metric]
343  )
344 
345  @staticmethod
346  def state_as_number(state: State) -> float | None:
347  """Return state as a float, or None if state cannot be converted."""
348  try:
349  if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP:
350  value = as_timestamp(state.state)
351  else:
352  value = state_helper.state_as_number(state)
353  except ValueError:
354  _LOGGER.debug("Could not convert %s to float", state)
355  value = None
356  return value
357 
358  @staticmethod
359  def _labels(state: State) -> dict[str, Any]:
360  return {
361  "entity": state.entity_id,
362  "domain": state.domain,
363  "friendly_name": state.attributes.get(ATTR_FRIENDLY_NAME),
364  }
365 
366  def _battery(self, state: State) -> None:
367  if (battery_level := state.attributes.get(ATTR_BATTERY_LEVEL)) is not None:
368  metric = self._metric(
369  "battery_level_percent",
370  prometheus_client.Gauge,
371  "Battery level as a percentage of its capacity",
372  )
373  try:
374  value = float(battery_level)
375  metric.labels(**self._labels_labels(state)).set(value)
376  except ValueError:
377  pass
378 
379  def _handle_binary_sensor(self, state: State) -> None:
380  metric = self._metric(
381  "binary_sensor_state",
382  prometheus_client.Gauge,
383  "State of the binary sensor (0/1)",
384  )
385  if (value := self.state_as_numberstate_as_number(state)) is not None:
386  metric.labels(**self._labels_labels(state)).set(value)
387 
388  def _handle_input_boolean(self, state: State) -> None:
389  metric = self._metric(
390  "input_boolean_state",
391  prometheus_client.Gauge,
392  "State of the input boolean (0/1)",
393  )
394  if (value := self.state_as_numberstate_as_number(state)) is not None:
395  metric.labels(**self._labels_labels(state)).set(value)
396 
397  def _numeric_handler(self, state: State, domain: str, title: str) -> None:
398  if unit := self._unit_string_unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
399  metric = self._metric(
400  f"{domain}_state_{unit}",
401  prometheus_client.Gauge,
402  f"State of the {title} measured in {unit}",
403  )
404  else:
405  metric = self._metric(
406  f"{domain}_state",
407  prometheus_client.Gauge,
408  f"State of the {title}",
409  )
410 
411  if (value := self.state_as_numberstate_as_number(state)) is not None:
412  if (
413  state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
414  == UnitOfTemperature.FAHRENHEIT
415  ):
416  value = TemperatureConverter.convert(
417  value, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
418  )
419  metric.labels(**self._labels_labels(state)).set(value)
420 
421  def _handle_input_number(self, state: State) -> None:
422  self._numeric_handler_numeric_handler(state, "input_number", "input number")
423 
424  def _handle_number(self, state: State) -> None:
425  self._numeric_handler_numeric_handler(state, "number", "number")
426 
427  def _handle_device_tracker(self, state: State) -> None:
428  metric = self._metric(
429  "device_tracker_state",
430  prometheus_client.Gauge,
431  "State of the device tracker (0/1)",
432  )
433  if (value := self.state_as_numberstate_as_number(state)) is not None:
434  metric.labels(**self._labels_labels(state)).set(value)
435 
436  def _handle_person(self, state: State) -> None:
437  metric = self._metric(
438  "person_state", prometheus_client.Gauge, "State of the person (0/1)"
439  )
440  if (value := self.state_as_numberstate_as_number(state)) is not None:
441  metric.labels(**self._labels_labels(state)).set(value)
442 
443  def _handle_cover(self, state: State) -> None:
444  metric = self._metric(
445  "cover_state",
446  prometheus_client.Gauge,
447  "State of the cover (0/1)",
448  ["state"],
449  )
450 
451  cover_states = [STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING]
452  for cover_state in cover_states:
453  metric.labels(**dict(self._labels_labels(state), state=cover_state)).set(
454  float(cover_state == state.state)
455  )
456 
457  position = state.attributes.get(ATTR_CURRENT_POSITION)
458  if position is not None:
459  position_metric = self._metric(
460  "cover_position",
461  prometheus_client.Gauge,
462  "Position of the cover (0-100)",
463  )
464  position_metric.labels(**self._labels_labels(state)).set(float(position))
465 
466  tilt_position = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
467  if tilt_position is not None:
468  tilt_position_metric = self._metric(
469  "cover_tilt_position",
470  prometheus_client.Gauge,
471  "Tilt Position of the cover (0-100)",
472  )
473  tilt_position_metric.labels(**self._labels_labels(state)).set(float(tilt_position))
474 
475  def _handle_light(self, state: State) -> None:
476  metric = self._metric(
477  "light_brightness_percent",
478  prometheus_client.Gauge,
479  "Light brightness percentage (0..100)",
480  )
481 
482  if (value := self.state_as_numberstate_as_number(state)) is not None:
483  brightness = state.attributes.get(ATTR_BRIGHTNESS)
484  if state.state == STATE_ON and brightness is not None:
485  value = float(brightness) / 255.0
486  value = value * 100
487  metric.labels(**self._labels_labels(state)).set(value)
488 
489  def _handle_lock(self, state: State) -> None:
490  metric = self._metric(
491  "lock_state", prometheus_client.Gauge, "State of the lock (0/1)"
492  )
493  if (value := self.state_as_numberstate_as_number(state)) is not None:
494  metric.labels(**self._labels_labels(state)).set(value)
495 
497  self, state: State, attr: str, metric_name: str, metric_description: str
498  ) -> None:
499  if (temp := state.attributes.get(attr)) is not None:
500  if self._climate_units_climate_units == UnitOfTemperature.FAHRENHEIT:
501  temp = TemperatureConverter.convert(
502  temp, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
503  )
504  metric = self._metric(
505  metric_name,
506  prometheus_client.Gauge,
507  metric_description,
508  )
509  metric.labels(**self._labels_labels(state)).set(temp)
510 
511  def _handle_climate(self, state: State) -> None:
512  self._handle_climate_temp_handle_climate_temp(
513  state,
514  ATTR_TEMPERATURE,
515  "climate_target_temperature_celsius",
516  "Target temperature in degrees Celsius",
517  )
518  self._handle_climate_temp_handle_climate_temp(
519  state,
520  ATTR_TARGET_TEMP_HIGH,
521  "climate_target_temperature_high_celsius",
522  "Target high temperature in degrees Celsius",
523  )
524  self._handle_climate_temp_handle_climate_temp(
525  state,
526  ATTR_TARGET_TEMP_LOW,
527  "climate_target_temperature_low_celsius",
528  "Target low temperature in degrees Celsius",
529  )
530  self._handle_climate_temp_handle_climate_temp(
531  state,
532  ATTR_CURRENT_TEMPERATURE,
533  "climate_current_temperature_celsius",
534  "Current temperature in degrees Celsius",
535  )
536 
537  if current_action := state.attributes.get(ATTR_HVAC_ACTION):
538  metric = self._metric(
539  "climate_action",
540  prometheus_client.Gauge,
541  "HVAC action",
542  ["action"],
543  )
544  for action in HVACAction:
545  metric.labels(**dict(self._labels_labels(state), action=action.value)).set(
546  float(action == current_action)
547  )
548 
549  current_mode = state.state
550  available_modes = state.attributes.get(ATTR_HVAC_MODES)
551  if current_mode and available_modes:
552  metric = self._metric(
553  "climate_mode",
554  prometheus_client.Gauge,
555  "HVAC mode",
556  ["mode"],
557  )
558  for mode in available_modes:
559  metric.labels(**dict(self._labels_labels(state), mode=mode)).set(
560  float(mode == current_mode)
561  )
562 
563  preset_mode = state.attributes.get(ATTR_PRESET_MODE)
564  available_preset_modes = state.attributes.get(ATTR_PRESET_MODES)
565  if preset_mode and available_preset_modes:
566  preset_metric = self._metric(
567  "climate_preset_mode",
568  prometheus_client.Gauge,
569  "Preset mode enum",
570  ["mode"],
571  )
572  for mode in available_preset_modes:
573  preset_metric.labels(**dict(self._labels_labels(state), mode=mode)).set(
574  float(mode == preset_mode)
575  )
576 
577  fan_mode = state.attributes.get(ATTR_FAN_MODE)
578  available_fan_modes = state.attributes.get(ATTR_FAN_MODES)
579  if fan_mode and available_fan_modes:
580  fan_mode_metric = self._metric(
581  "climate_fan_mode",
582  prometheus_client.Gauge,
583  "Fan mode enum",
584  ["mode"],
585  )
586  for mode in available_fan_modes:
587  fan_mode_metric.labels(**dict(self._labels_labels(state), mode=mode)).set(
588  float(mode == fan_mode)
589  )
590 
591  def _handle_humidifier(self, state: State) -> None:
592  humidifier_target_humidity_percent = state.attributes.get(ATTR_HUMIDITY)
593  if humidifier_target_humidity_percent:
594  metric = self._metric(
595  "humidifier_target_humidity_percent",
596  prometheus_client.Gauge,
597  "Target Relative Humidity",
598  )
599  metric.labels(**self._labels_labels(state)).set(humidifier_target_humidity_percent)
600 
601  metric = self._metric(
602  "humidifier_state",
603  prometheus_client.Gauge,
604  "State of the humidifier (0/1)",
605  )
606  if (value := self.state_as_numberstate_as_number(state)) is not None:
607  metric.labels(**self._labels_labels(state)).set(value)
608 
609  current_mode = state.attributes.get(ATTR_MODE)
610  available_modes = state.attributes.get(ATTR_AVAILABLE_MODES)
611  if current_mode and available_modes:
612  metric = self._metric(
613  "humidifier_mode",
614  prometheus_client.Gauge,
615  "Humidifier Mode",
616  ["mode"],
617  )
618  for mode in available_modes:
619  metric.labels(**dict(self._labels_labels(state), mode=mode)).set(
620  float(mode == current_mode)
621  )
622 
623  def _handle_sensor(self, state: State) -> None:
624  unit = self._unit_string_unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT))
625 
626  for metric_handler in self._sensor_metric_handlers:
627  metric = metric_handler(state, unit)
628  if metric is not None:
629  break
630 
631  if metric is not None:
632  documentation = "State of the sensor"
633  if unit:
634  documentation = f"Sensor data measured in {unit}"
635 
636  _metric = self._metric(metric, prometheus_client.Gauge, documentation)
637 
638  if (value := self.state_as_numberstate_as_number(state)) is not None:
639  if (
640  state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
641  == UnitOfTemperature.FAHRENHEIT
642  ):
643  value = TemperatureConverter.convert(
644  value, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
645  )
646  _metric.labels(**self._labels_labels(state)).set(value)
647 
648  self._battery_battery(state)
649 
650  def _sensor_default_metric(self, state: State, unit: str | None) -> str | None:
651  """Get default metric."""
652  return self._default_metric_default_metric
653 
654  @staticmethod
655  def _sensor_attribute_metric(state: State, unit: str | None) -> str | None:
656  """Get metric based on device class attribute."""
657  metric = state.attributes.get(ATTR_DEVICE_CLASS)
658  if metric is not None:
659  return f"sensor_{metric}_{unit}"
660  return None
661 
662  @staticmethod
663  def _sensor_timestamp_metric(state: State, unit: str | None) -> str | None:
664  """Get metric for timestamp sensors, which have no unit of measurement attribute."""
665  metric = state.attributes.get(ATTR_DEVICE_CLASS)
666  if metric == SensorDeviceClass.TIMESTAMP:
667  return f"sensor_{metric}_seconds"
668  return None
669 
670  def _sensor_override_metric(self, state: State, unit: str | None) -> str | None:
671  """Get metric from override in configuration."""
672  if self._override_metric_override_metric:
673  return self._override_metric_override_metric
674  return None
675 
677  self, state: State, unit: str | None
678  ) -> str | None:
679  """Get metric from override in component configuration."""
680  return self._component_config_component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC)
681 
682  @staticmethod
683  def _sensor_fallback_metric(state: State, unit: str | None) -> str | None:
684  """Get metric from fallback logic for compatibility."""
685  if unit not in (None, ""):
686  return f"sensor_unit_{unit}"
687  return "sensor_state"
688 
689  @staticmethod
690  def _unit_string(unit: str | None) -> str | None:
691  """Get a formatted string of the unit."""
692  if unit is None:
693  return None
694 
695  units = {
696  UnitOfTemperature.CELSIUS: "celsius",
697  UnitOfTemperature.FAHRENHEIT: "celsius", # F should go into C metric
698  PERCENTAGE: "percent",
699  }
700  default = unit.replace("/", "_per_")
701  default = default.lower()
702  return units.get(unit, default)
703 
704  def _handle_switch(self, state: State) -> None:
705  metric = self._metric(
706  "switch_state", prometheus_client.Gauge, "State of the switch (0/1)"
707  )
708 
709  if (value := self.state_as_numberstate_as_number(state)) is not None:
710  metric.labels(**self._labels_labels(state)).set(value)
711 
712  self._handle_attributes_handle_attributes(state)
713 
714  def _handle_fan(self, state: State) -> None:
715  metric = self._metric(
716  "fan_state", prometheus_client.Gauge, "State of the fan (0/1)"
717  )
718 
719  if (value := self.state_as_numberstate_as_number(state)) is not None:
720  metric.labels(**self._labels_labels(state)).set(value)
721 
722  fan_speed_percent = state.attributes.get(ATTR_PERCENTAGE)
723  if fan_speed_percent is not None:
724  fan_speed_metric = self._metric(
725  "fan_speed_percent",
726  prometheus_client.Gauge,
727  "Fan speed percent (0-100)",
728  )
729  fan_speed_metric.labels(**self._labels_labels(state)).set(float(fan_speed_percent))
730 
731  fan_is_oscillating = state.attributes.get(ATTR_OSCILLATING)
732  if fan_is_oscillating is not None:
733  fan_oscillating_metric = self._metric(
734  "fan_is_oscillating",
735  prometheus_client.Gauge,
736  "Whether the fan is oscillating (0/1)",
737  )
738  fan_oscillating_metric.labels(**self._labels_labels(state)).set(
739  float(fan_is_oscillating)
740  )
741 
742  fan_preset_mode = state.attributes.get(ATTR_PRESET_MODE)
743  available_modes = state.attributes.get(ATTR_PRESET_MODES)
744  if fan_preset_mode and available_modes:
745  fan_preset_metric = self._metric(
746  "fan_preset_mode",
747  prometheus_client.Gauge,
748  "Fan preset mode enum",
749  ["mode"],
750  )
751  for mode in available_modes:
752  fan_preset_metric.labels(**dict(self._labels_labels(state), mode=mode)).set(
753  float(mode == fan_preset_mode)
754  )
755 
756  fan_direction = state.attributes.get(ATTR_DIRECTION)
757  if fan_direction is not None:
758  fan_direction_metric = self._metric(
759  "fan_direction_reversed",
760  prometheus_client.Gauge,
761  "Fan direction reversed (bool)",
762  )
763  if fan_direction == DIRECTION_FORWARD:
764  fan_direction_metric.labels(**self._labels_labels(state)).set(0)
765  elif fan_direction == DIRECTION_REVERSE:
766  fan_direction_metric.labels(**self._labels_labels(state)).set(1)
767 
768  def _handle_zwave(self, state: State) -> None:
769  self._battery_battery(state)
770 
771  def _handle_automation(self, state: State) -> None:
772  metric = self._metric(
773  "automation_triggered_count",
774  prometheus_client.Counter,
775  "Count of times an automation has been triggered",
776  )
777 
778  metric.labels(**self._labels_labels(state)).inc()
779 
780  def _handle_counter(self, state: State) -> None:
781  metric = self._metric(
782  "counter_value",
783  prometheus_client.Gauge,
784  "Value of counter entities",
785  )
786  if (value := self.state_as_numberstate_as_number(state)) is not None:
787  metric.labels(**self._labels_labels(state)).set(value)
788 
789  def _handle_update(self, state: State) -> None:
790  metric = self._metric(
791  "update_state",
792  prometheus_client.Gauge,
793  "Update state, indicating if an update is available (0/1)",
794  )
795  if (value := self.state_as_numberstate_as_number(state)) is not None:
796  metric.labels(**self._labels_labels(state)).set(value)
797 
798  def _handle_alarm_control_panel(self, state: State) -> None:
799  current_state = state.state
800 
801  if current_state:
802  metric = self._metric(
803  "alarm_control_panel_state",
804  prometheus_client.Gauge,
805  "State of the alarm control panel (0/1)",
806  ["state"],
807  )
808 
809  for alarm_state in AlarmControlPanelState:
810  metric.labels(**dict(self._labels_labels(state), state=alarm_state.value)).set(
811  float(alarm_state.value == current_state)
812  )
813 
814 
815 class PrometheusView(HomeAssistantView):
816  """Handle Prometheus requests."""
817 
818  url = API_ENDPOINT
819  name = "api:prometheus"
820 
821  def __init__(self, requires_auth: bool) -> None:
822  """Initialize Prometheus view."""
823  self.requires_authrequires_auth = requires_auth
824 
825  async def get(self, request: web.Request) -> web.Response:
826  """Handle request for Prometheus metrics."""
827  _LOGGER.debug("Received Prometheus metrics request")
828 
829  hass = request.app[KEY_HASS]
830  body = await hass.async_add_executor_job(
831  prometheus_client.generate_latest, prometheus_client.REGISTRY
832  )
833  return web.Response(
834  body=body,
835  content_type=CONTENT_TYPE_TEXT_PLAIN,
836  )
None __init__(self, entityfilter.EntityFilter entity_filter, str namespace, UnitOfTemperature climate_units, EntityValues component_config, str|None override_metric, str|None default_metric)
Definition: __init__.py:172
None handle_state_changed_event(self, Event[EventStateChangedData] event)
Definition: __init__.py:196
str|None _sensor_timestamp_metric(State state, str|None unit)
Definition: __init__.py:663
None _numeric_handler(self, State state, str domain, str title)
Definition: __init__.py:397
str|None _sensor_override_metric(self, State state, str|None unit)
Definition: __init__.py:670
str|None _sensor_override_component_metric(self, State state, str|None unit)
Definition: __init__.py:678
str|None _sensor_fallback_metric(State state, str|None unit)
Definition: __init__.py:683
str|None _sensor_default_metric(self, State state, str|None unit)
Definition: __init__.py:650
None handle_entity_registry_updated(self, Event[EventEntityRegistryUpdatedData] event)
Definition: __init__.py:251
None _handle_climate_temp(self, State state, str attr, str metric_name, str metric_description)
Definition: __init__.py:498
str|None _sensor_attribute_metric(State state, str|None unit)
Definition: __init__.py:655
None _remove_labelsets(self, str entity_id, str|None friendly_name=None, set[MetricWrapperBase]|None ignored_metrics=None)
Definition: __init__.py:279
None __init__(self, bool requires_auth)
Definition: __init__.py:821
web.Response get(self, web.Request request)
Definition: __init__.py:825
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:123
float as_timestamp(dt.datetime|str dt_value)
Definition: dt.py:145