Avoid creating Prometheus metrics for non-numeric states (#127262)

This commit is contained in:
Anton Tolchanov 2024-10-25 12:31:30 +01:00 committed by GitHub
parent cca6965cd1
commit 6d48316436
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 46 additions and 50 deletions

View File

@ -334,8 +334,8 @@ class PrometheusMetrics:
)
@staticmethod
def state_as_number(state: State) -> float:
"""Return a state casted to a float."""
def state_as_number(state: State) -> float | None:
"""Return state as a float, or None if state cannot be converted."""
try:
if state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP:
value = as_timestamp(state.state)
@ -343,7 +343,7 @@ class PrometheusMetrics:
value = state_helper.state_as_number(state)
except ValueError:
_LOGGER.debug("Could not convert %s to float", state)
value = 0
value = None
return value
@staticmethod
@ -373,8 +373,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the binary sensor (0/1)",
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_input_boolean(self, state: State) -> None:
metric = self._metric(
@ -382,8 +382,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the input boolean (0/1)",
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _numeric_handler(self, state: State, domain: str, title: str) -> None:
if unit := self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
@ -399,8 +399,7 @@ class PrometheusMetrics:
f"State of the {title}",
)
with suppress(ValueError):
value = self.state_as_number(state)
if (value := self.state_as_number(state)) is not None:
if (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfTemperature.FAHRENHEIT
@ -422,15 +421,15 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the device tracker (0/1)",
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_person(self, state: State) -> None:
metric = self._metric(
"person_state", prometheus_client.Gauge, "State of the person (0/1)"
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_cover(self, state: State) -> None:
metric = self._metric(
@ -471,23 +470,19 @@ class PrometheusMetrics:
"Light brightness percentage (0..100)",
)
try:
if (value := self.state_as_number(state)) is not None:
brightness = state.attributes.get(ATTR_BRIGHTNESS)
if state.state == STATE_ON and brightness is not None:
value = brightness / 255.0
else:
value = self.state_as_number(state)
value = float(brightness) / 255.0
value = value * 100
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
def _handle_lock(self, state: State) -> None:
metric = self._metric(
"lock_state", prometheus_client.Gauge, "State of the lock (0/1)"
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_climate_temp(
self, state: State, attr: str, metric_name: str, metric_description: str
@ -599,11 +594,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"State of the humidifier (0/1)",
)
try:
value = self.state_as_number(state)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
current_mode = state.attributes.get(ATTR_MODE)
available_modes = state.attributes.get(ATTR_AVAILABLE_MODES)
@ -634,8 +626,7 @@ class PrometheusMetrics:
_metric = self._metric(metric, prometheus_client.Gauge, documentation)
try:
value = self.state_as_number(state)
if (value := self.state_as_number(state)) is not None:
if (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== UnitOfTemperature.FAHRENHEIT
@ -644,8 +635,6 @@ class PrometheusMetrics:
value, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS
)
_metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
self._battery(state)
@ -684,14 +673,9 @@ class PrometheusMetrics:
@staticmethod
def _sensor_fallback_metric(state: State, unit: str | None) -> str | None:
"""Get metric from fallback logic for compatibility."""
if unit in (None, ""):
try:
state_helper.state_as_number(state)
except ValueError:
_LOGGER.debug("Unsupported sensor: %s", state.entity_id)
return None
return "sensor_state"
return f"sensor_unit_{unit}"
if unit not in (None, ""):
return f"sensor_unit_{unit}"
return "sensor_state"
@staticmethod
def _unit_string(unit: str | None) -> str | None:
@ -713,11 +697,8 @@ class PrometheusMetrics:
"switch_state", prometheus_client.Gauge, "State of the switch (0/1)"
)
try:
value = self.state_as_number(state)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
self._handle_attributes(state)
@ -726,11 +707,8 @@ class PrometheusMetrics:
"fan_state", prometheus_client.Gauge, "State of the fan (0/1)"
)
try:
value = self.state_as_number(state)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
except ValueError:
pass
fan_speed_percent = state.attributes.get(ATTR_PERCENTAGE)
if fan_speed_percent is not None:
@ -796,8 +774,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"Value of counter entities",
)
metric.labels(**self._labels(state)).set(self.state_as_number(state))
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_update(self, state: State) -> None:
metric = self._metric(
@ -805,8 +783,8 @@ class PrometheusMetrics:
prometheus_client.Gauge,
"Update state, indicating if an update is available (0/1)",
)
value = self.state_as_number(state)
metric.labels(**self._labels(state)).set(value)
if (value := self.state_as_number(state)) is not None:
metric.labels(**self._labels(state)).set(value)
def _handle_alarm_control_panel(self, state: State) -> None:
current_state = state.state

View File

@ -642,7 +642,7 @@ async def test_sensor_without_unit(
domain="sensor",
friendly_name="Text Unit",
entity="sensor.text_unit",
).withValue(0.0).assert_in_metrics(body)
).assert_not_in_metrics(body)
@pytest.mark.parametrize("namespace", [""])
@ -716,6 +716,13 @@ async def test_input_number(
entity="input_number.target_temperature",
).withValue(22.7).assert_in_metrics(body)
EntityMetric(
metric_name="input_number_state_celsius",
domain="input_number",
friendly_name="Converted temperature",
entity="input_number.converted_temperature",
).withValue(100).assert_in_metrics(body)
@pytest.mark.parametrize("namespace", [""])
async def test_number(
@ -2207,6 +2214,17 @@ async def input_number_fixture(
set_state_with_entry(hass, input_number_3, 22.7)
data["input_number_3"] = input_number_3
input_number_4 = entity_registry.async_get_or_create(
domain=input_number.DOMAIN,
platform="test",
unique_id="input_number_4",
suggested_object_id="converted_temperature",
original_name="Converted temperature",
unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
)
set_state_with_entry(hass, input_number_4, 212)
data["input_number_4"] = input_number_4
await hass.async_block_till_done()
return data