diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 3624ebb7ef2..ea279a01dc6 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -39,6 +39,7 @@ from .valve import async_create_preview_valve _STATISTIC_MEASURES = [ "last", + "first_available", "max", "mean", "median", diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py index 078b6a29739..2be61620b95 100644 --- a/homeassistant/components/group/sensor.py +++ b/homeassistant/components/group/sensor.py @@ -68,6 +68,8 @@ ATTR_MEAN = "mean" ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_FIRST_AVAILABLE = "first_available" +ATTR_FIRST_AVAILABLE_ENTITY_ID = "first_available_entity_id" ATTR_RANGE = "range" ATTR_STDEV = "stdev" ATTR_SUM = "sum" @@ -78,6 +80,7 @@ SENSOR_TYPES = { ATTR_MEAN: "mean", ATTR_MEDIAN: "median", ATTR_LAST: "last", + ATTR_FIRST_AVAILABLE: "first_available", ATTR_RANGE: "range", ATTR_STDEV: "stdev", ATTR_SUM: "sum", @@ -255,6 +258,19 @@ def calc_last( return attributes, last +def calc_first_available( + sensor_values: list[tuple[str, float, State]], +) -> tuple[dict[str, str | None], float | None]: + """Calculate first available value.""" + first_available_entity_id: str | None = None + first_available: float | None = None + if sensor_values: + first_available_entity_id, first_available, _ = sensor_values[0] + + attributes = {ATTR_FIRST_AVAILABLE_ENTITY_ID: first_available_entity_id} + return attributes, first_available + + def calc_range( sensor_values: list[tuple[str, float, State]], ) -> tuple[dict[str, str | None], float]: @@ -309,6 +325,7 @@ CALC_TYPES: dict[ "mean": calc_mean, "median": calc_median, "last": calc_last, + "first_available": calc_first_available, "range": calc_range, "stdev": calc_stdev, "sum": calc_sum, diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index ba4cb242700..5c8aee6b2d2 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -280,6 +280,7 @@ "selector": { "type": { "options": { + "first_available": "First available", "last": "Most recently updated", "max": "Maximum", "mean": "Arithmetic mean", diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py index dafdbbb9c3d..69b19b05f77 100644 --- a/tests/components/group/test_sensor.py +++ b/tests/components/group/test_sensor.py @@ -12,6 +12,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN from homeassistant.components.group.sensor import ( + ATTR_FIRST_AVAILABLE_ENTITY_ID, ATTR_LAST_ENTITY_ID, ATTR_MAX_ENTITY_ID, ATTR_MIN_ENTITY_ID, @@ -86,6 +87,11 @@ def set_or_remove_state( ("mean", MEAN, {}), ("median", MEDIAN, {}), ("last", VALUES[2], {ATTR_LAST_ENTITY_ID: "sensor.test_3"}), + ( + "first_available", + VALUES[0], + {ATTR_FIRST_AVAILABLE_ENTITY_ID: "sensor.test_1"}, + ), ("range", RANGE, {}), ("stdev", STDEV, {}), ("sum", SUM_VALUE, {}), @@ -861,6 +867,62 @@ async def test_last_sensor(hass: HomeAssistant) -> None: assert state.attributes.get("last_entity_id") == entity_id +async def test_first_available_sensor(hass: HomeAssistant) -> None: + """Test the first available sensor.""" + config = { + SENSOR_DOMAIN: { + "platform": DOMAIN, + "name": "test_first_available", + "type": "first_available", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_first_available_sensor", + "ignore_non_numeric": True, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + # Ensure that while sensor states are being set + # the group will always point to the first available sensor. + + for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_first_available") + assert str(float(VALUES[0])) == state.state + assert entity_ids[0] == state.attributes.get("first_available_entity_id") + + # If the second sensor of the group becomes unavailable + # then the first one should still be taken. + + hass.states.async_set(entity_ids[1], STATE_UNAVAILABLE) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_first_available") + assert str(float(VALUES[0])) == state.state + assert entity_ids[0] == state.attributes.get("first_available_entity_id") + + # If the first sensor of the group becomes now unavailable + # then the third one should be taken. + + hass.states.async_set(entity_ids[0], STATE_UNAVAILABLE) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_first_available") + assert str(float(VALUES[2])) == state.state + assert entity_ids[2] == state.attributes.get("first_available_entity_id") + + # If all sensors of the group become unavailable + # then the group should also be unavailable. + + hass.states.async_set(entity_ids[2], STATE_UNAVAILABLE) + await hass.async_block_till_done() + state = hass.states.get("sensor.test_first_available") + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get("first_available_entity_id") is None + + async def test_sensors_attributes_added_when_entity_info_available( hass: HomeAssistant, ) -> None: