From 275a7499b1d4fba956811873994b416d6b72b8b3 Mon Sep 17 00:00:00 2001 From: Elliott Balsley <3991046+llamafilm@users.noreply.github.com> Date: Sun, 7 Jul 2024 07:32:15 -0700 Subject: [PATCH] Add prometheus fan handler (#119805) Co-authored-by: Anton Tolchanov <1687799+knyar@users.noreply.github.com> --- .../components/prometheus/__init__.py | 66 +++++++++++++ tests/components/prometheus/test_init.py | 95 +++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index a0f0d69ce46..1ac3442e21e 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -26,6 +26,15 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ) +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DIRECTION_FORWARD, + DIRECTION_REVERSE, +) from homeassistant.components.http import KEY_HASS, HomeAssistantView from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.components.light import ATTR_BRIGHTNESS @@ -684,6 +693,63 @@ class PrometheusMetrics: self._handle_attributes(state) + def _handle_fan(self, state: State) -> None: + metric = self._metric( + "fan_state", prometheus_client.Gauge, "State of the fan (0/1)" + ) + + try: + value = self.state_as_number(state) + 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: + fan_speed_metric = self._metric( + "fan_speed_percent", + prometheus_client.Gauge, + "Fan speed percent (0-100)", + ) + fan_speed_metric.labels(**self._labels(state)).set(float(fan_speed_percent)) + + fan_is_oscillating = state.attributes.get(ATTR_OSCILLATING) + if fan_is_oscillating is not None: + fan_oscillating_metric = self._metric( + "fan_is_oscillating", + prometheus_client.Gauge, + "Whether the fan is oscillating (0/1)", + ) + fan_oscillating_metric.labels(**self._labels(state)).set( + float(fan_is_oscillating) + ) + + fan_preset_mode = state.attributes.get(ATTR_PRESET_MODE) + available_modes = state.attributes.get(ATTR_PRESET_MODES) + if fan_preset_mode and available_modes: + fan_preset_metric = self._metric( + "fan_preset_mode", + prometheus_client.Gauge, + "Fan preset mode enum", + ["mode"], + ) + for mode in available_modes: + fan_preset_metric.labels(**dict(self._labels(state), mode=mode)).set( + float(mode == fan_preset_mode) + ) + + fan_direction = state.attributes.get(ATTR_DIRECTION) + if fan_direction is not None: + fan_direction_metric = self._metric( + "fan_direction_reversed", + prometheus_client.Gauge, + "Fan direction reversed (bool)", + ) + if fan_direction == DIRECTION_FORWARD: + fan_direction_metric.labels(**self._labels(state)).set(0) + elif fan_direction == DIRECTION_REVERSE: + fan_direction_metric.labels(**self._labels(state)).set(1) + def _handle_zwave(self, state: State) -> None: self._battery(state) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 499d1a5df14..daedef54780 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components import ( counter, cover, device_tracker, + fan, humidifier, input_boolean, input_number, @@ -35,6 +36,15 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ) +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DIRECTION_FORWARD, + DIRECTION_REVERSE, +) from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( @@ -562,6 +572,51 @@ async def test_lock( ) +@pytest.mark.parametrize("namespace", [""]) +async def test_fan( + client: ClientSessionGenerator, fan_entities: dict[str, er.RegistryEntry] +) -> None: + """Test prometheus metrics for fan.""" + body = await generate_latest_metrics(client) + + assert ( + 'fan_state{domain="fan",' + 'entity="fan.fan_1",' + 'friendly_name="Fan 1"} 1.0' in body + ) + + assert ( + 'fan_speed_percent{domain="fan",' + 'entity="fan.fan_1",' + 'friendly_name="Fan 1"} 33.0' in body + ) + + assert ( + 'fan_is_oscillating{domain="fan",' + 'entity="fan.fan_1",' + 'friendly_name="Fan 1"} 1.0' in body + ) + + assert ( + 'fan_direction_reversed{domain="fan",' + 'entity="fan.fan_1",' + 'friendly_name="Fan 1"} 0.0' in body + ) + + assert ( + 'fan_preset_mode{domain="fan",' + 'entity="fan.fan_1",' + 'friendly_name="Fan 1",' + 'mode="LO"} 1.0' in body + ) + + assert ( + 'fan_direction_reversed{domain="fan",' + 'entity="fan.fan_2",' + 'friendly_name="Reverse Fan"} 1.0' in body + ) + + @pytest.mark.parametrize("namespace", [""]) async def test_cover( client: ClientSessionGenerator, cover_entities: dict[str, er.RegistryEntry] @@ -1788,6 +1843,46 @@ async def switch_fixture( return data +@pytest.fixture(name="fan_entities") +async def fan_fixture( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> dict[str, er.RegistryEntry]: + """Simulate fan entities.""" + data = {} + fan_1 = entity_registry.async_get_or_create( + domain=fan.DOMAIN, + platform="test", + unique_id="fan_1", + suggested_object_id="fan_1", + original_name="Fan 1", + ) + fan_1_attributes = { + ATTR_DIRECTION: DIRECTION_FORWARD, + ATTR_OSCILLATING: True, + ATTR_PERCENTAGE: 33, + ATTR_PRESET_MODE: "LO", + ATTR_PRESET_MODES: ["LO", "OFF", "HI"], + } + set_state_with_entry(hass, fan_1, STATE_ON, fan_1_attributes) + data["fan_1"] = fan_1 + data["fan_1_attributes"] = fan_1_attributes + + fan_2 = entity_registry.async_get_or_create( + domain=fan.DOMAIN, + platform="test", + unique_id="fan_2", + suggested_object_id="fan_2", + original_name="Reverse Fan", + ) + fan_2_attributes = {ATTR_DIRECTION: DIRECTION_REVERSE} + set_state_with_entry(hass, fan_2, STATE_ON, fan_2_attributes) + data["fan_2"] = fan_2 + data["fan_2_attributes"] = fan_2_attributes + + await hass.async_block_till_done() + return data + + @pytest.fixture(name="person_entities") async def person_fixture( hass: HomeAssistant, entity_registry: er.EntityRegistry