diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 54a98a68e43..ff0e58badfb 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -132,7 +132,7 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): # filtered_states are members currently in the state machine filtered_states: list[str] = [x.state for x in all_states if x is not None] - # Set group as unavailable if all members are unavailable + # Set group as unavailable if all members are unavailable or missing self._attr_available = any( state != STATE_UNAVAILABLE for state in filtered_states ) diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index a0872b11f16..fbc19904faa 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -50,7 +50,13 @@ async def test_default_state(hass): async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, BINARY_SENSOR_DOMAIN, @@ -68,26 +74,12 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_OFF) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_ON) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + # Initial state with no group member in the state machine -> unavailable + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + # All group members unavailable -> unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -95,6 +87,12 @@ async def test_state_reporting_all(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) + # At least one member unknown or unavailable -> group unknown + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) await hass.async_block_till_done() @@ -105,9 +103,55 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # At least one member off -> group off + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + # Otherwise -> on + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("binary_sensor.test1") + hass.states.async_remove("binary_sensor.test2") + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + async def test_state_reporting_any(hass): - """Test the state reporting.""" + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, BINARY_SENSOR_DOMAIN, @@ -126,26 +170,17 @@ async def test_state_reporting_any(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("binary_sensor.binary_sensor_group") + assert entry + assert entry.unique_id == "unique_identifier" - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - - hass.states.async_set("binary_sensor.test1", STATE_OFF) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_ON) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + # Initial state with no group member in the state machine -> unavailable + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + # All group members unavailable -> unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -153,17 +188,59 @@ async def test_state_reporting_any(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) - entity_registry = er.async_get(hass) - entry = entity_registry.async_get("binary_sensor.binary_sensor_group") - assert entry - assert entry.unique_id == "unique_identifier" + # All group members unknown -> unknown + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) - hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + # Otherwise -> off + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_OFF) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("binary_sensor.test1") + hass.states.async_remove("binary_sensor.test2") + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index d090141a9d2..83c85a70b63 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -33,6 +33,7 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er @@ -99,7 +100,14 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_state(hass, setup_comp): - """Test handling of state.""" + """Test handling of state. + + The group state is unknown if all group members are unknown or unavailable. + Otherwise, the group state is opening if at least one group member is opening. + Otherwise, the group state is closing if at least one group member is closing. + Otherwise, the group state is open if at least one group member is open. + Otherwise, the group state is closed. + """ state = hass.states.get(COVER_GROUP) # No entity has a valid state -> group state unknown assert state.state == STATE_UNKNOWN @@ -115,87 +123,125 @@ async def test_state(hass, setup_comp): assert ATTR_CURRENT_POSITION not in state.attributes assert ATTR_CURRENT_TILT_POSITION not in state.attributes - # Set all entities as closed -> group state closed - hass.states.async_set(DEMO_COVER, STATE_CLOSED, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_CLOSED + # The group state is unknown if all group members are unknown or unavailable. + for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNKNOWN - # Set all entities as open -> group state open - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_OPEN, {}) - hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPEN + for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNKNOWN - # Set first entity as open -> group state open - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPEN + # At least one member opening -> group opening + for state_1 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_2 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_3 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_OPENING, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING - # Set last entity as open -> group state open - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPEN + # At least one member closing -> group closing + for state_1 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_2 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_3 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSING, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSING - # Set conflicting valid states -> opening state has priority - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPENING + # At least one member open -> group open + for state_1 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN - # Set all entities to unknown state -> group state unknown - hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {}) + # At least one member closed -> group closed + for state_1 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + + # All group members removed from the state machine -> unknown + hass.states.async_remove(DEMO_COVER) + hass.states.async_remove(DEMO_COVER_POS) + hass.states.async_remove(DEMO_COVER_TILT) + hass.states.async_remove(DEMO_TILT) await hass.async_block_till_done() state = hass.states.get(COVER_GROUP) assert state.state == STATE_UNKNOWN - # Set one entity to unknown state -> open state has priority - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) - hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPEN - - # Set one entity to unknown state -> opening state has priority - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_OPENING - - # Set one entity to unknown state -> closing state has priority - hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) - hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) - hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) - hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_CLOSING - @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_attributes(hass, setup_comp): diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 19b4fe4670a..8aefd12c93a 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -33,6 +33,8 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -111,7 +113,11 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_state(hass, setup_comp): - """Test handling of state.""" + """Test handling of state. + + The group state is on if at least one group member is on. + Otherwise, the group state is off. + """ state = hass.states.get(FAN_GROUP) # No entity has a valid state -> group state off assert state.state == STATE_OFF @@ -123,41 +129,55 @@ async def test_state(hass, setup_comp): assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - # Set all entities as on -> group state on - hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_ON, {}) - hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_ON + # The group state is off if all group members are off, unknown or unavailable. + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_OFF, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF - # Set all entities as off -> group state off - hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_OFF, {}) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_OFF + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE, {} + ) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF - # Set first entity as on -> group state on - hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_OFF, {}) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_ON + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {} + ) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF - # Set last entity as on -> group state on - hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) - hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_ON + # At least one member on -> group on + for state_1 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON # now remove an entity hass.states.async_remove(PERCENTAGE_LIMITED_FAN_ENTITY_ID) @@ -167,6 +187,16 @@ async def test_state(hass, setup_comp): assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # now remove all entities + hass.states.async_remove(CEILING_FAN_ENTITY_ID) + hass.states.async_remove(LIVING_ROOM_FAN_ENTITY_ID) + hass.states.async_remove(PERCENTAGE_FULL_FAN_ENTITY_ID) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # Test entity registry integration entity_registry = er.async_get(hass) entry = entity_registry.async_get(FAN_GROUP) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d5f7abedb44..f3083812553 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -88,8 +88,14 @@ async def test_default_state(hass): assert entry.unique_id == "unique_identifier" -async def test_state_reporting(hass): - """Test the state reporting.""" +async def test_state_reporting_any(hass): + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, LIGHT_DOMAIN, @@ -105,29 +111,79 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("light.test1", STATE_ON) - hass.states.async_set("light.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON - - hass.states.async_set("light.test1", STATE_ON) - hass.states.async_set("light.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON - - hass.states.async_set("light.test1", STATE_OFF) - hass.states.async_set("light.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_OFF + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + # All group members unavailable -> unavailable hass.states.async_set("light.test1", STATE_UNAVAILABLE) hass.states.async_set("light.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + # All group members unknown -> unknown + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + # Otherwise -> off + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("light.test1") + hass.states.async_remove("light.test2") + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, LIGHT_DOMAIN, @@ -143,11 +199,47 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + # At least one member unknown or unavailable -> group unknown hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNKNOWN + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # At least one member off -> group off hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_OFF) await hass.async_block_till_done() @@ -158,13 +250,15 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_OFF + # Otherwise -> on hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_ON) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_ON - hass.states.async_set("light.test1", STATE_UNAVAILABLE) - hass.states.async_set("light.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("light.test1") + hass.states.async_remove("light.test2") await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index 8db28fab18e..e76e47577c6 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -57,7 +57,16 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is jammed if at least one group member is jammed. + Otherwise, the group state is locking if at least one group member is locking. + Otherwise, the group state is unlocking if at least one group member is unlocking. + Otherwise, the group state is unlocked if at least one group member is unlocked. + Otherwise, the group state is locked. + """ await async_setup_component( hass, LOCK_DOMAIN, @@ -72,43 +81,98 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("lock.test1", STATE_LOCKED) + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("lock.test1", STATE_UNAVAILABLE) hass.states.async_set("lock.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE - hass.states.async_set("lock.test1", STATE_LOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + # At least one member unknown or unavailable -> group unknown + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNKNOWN, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + + # At least one member jammed -> group jammed + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_JAMMED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_JAMMED + + # At least one member locking -> group unlocking + for state_1 in ( + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_LOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_LOCKING + + # At least one member unlocking -> group unlocking + for state_1 in ( + STATE_LOCKED, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNLOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKING + + # At least one member unlocked -> group unlocked + for state_1 in ( + STATE_LOCKED, + STATE_UNLOCKED, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNLOCKED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + + # Otherwise -> locked hass.states.async_set("lock.test1", STATE_LOCKED) hass.states.async_set("lock.test2", STATE_LOCKED) await hass.async_block_till_done() assert hass.states.get("lock.lock_group").state == STATE_LOCKED - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED - - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_JAMMED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_JAMMED - - hass.states.async_set("lock.test1", STATE_LOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKING) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKING - - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_LOCKING) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_LOCKING - - hass.states.async_set("lock.test1", STATE_UNAVAILABLE) - hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("lock.test1") + hass.states.async_remove("lock.test2") await hass.async_block_till_done() assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index f741e2d1a84..85e75ffcba6 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -43,6 +43,8 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, + STATE_BUFFERING, + STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -99,7 +101,17 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is buffering if all group members are buffering. + Otherwise, the group state is idle if all group members are idle. + Otherwise, the group state is paused if all group members are paused. + Otherwise, the group state is playing if all group members are playing. + Otherwise, the group state is on if at least one group member is not off, unavailable or unknown. + Otherwise, the group state is off. + """ await async_setup_component( hass, MEDIA_DOMAIN, @@ -114,27 +126,60 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unknown assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN - hass.states.async_set("media_player.player_1", STATE_ON) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_ON + # All group members buffering -> buffering + # All group members idle -> idle + # All group members paused -> paused + # All group members playing -> playing + # All group members unavailable -> unavailable + # All group members unknown -> unknown + for state in ( + STATE_BUFFERING, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set("media_player.player_1", state) + hass.states.async_set("media_player.player_2", state) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == state - hass.states.async_set("media_player.player_1", STATE_ON) - hass.states.async_set("media_player.player_2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_ON + # At least one member not off, unavailable or unknown -> on + for state_1 in (STATE_BUFFERING, STATE_IDLE, STATE_ON, STATE_PAUSED, STATE_PLAYING): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", state_2) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_ON - hass.states.async_set("media_player.player_1", STATE_OFF) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_OFF + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF - hass.states.async_set("media_player.player_1", STATE_UNAVAILABLE) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + # Otherwise off + for state_1 in (STATE_OFF, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF + + for state_1 in (STATE_OFF, STATE_UNAVAILABLE): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF + + # All group members removed from the state machine -> unknown + hass.states.async_remove("media_player.player_1") + hass.states.async_remove("media_player.player_2") await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE + assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN async def test_supported_features(hass): diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py index 5df2542d101..9a8da274a0a 100644 --- a/tests/components/group/test_switch.py +++ b/tests/components/group/test_switch.py @@ -56,7 +56,13 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, SWITCH_DOMAIN, @@ -72,29 +78,79 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("switch.test1", STATE_ON) - hass.states.async_set("switch.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_ON - - hass.states.async_set("switch.test1", STATE_ON) - hass.states.async_set("switch.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_ON - - hass.states.async_set("switch.test1", STATE_OFF) - hass.states.async_set("switch.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_OFF + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + # All group members unavailable -> unavailable hass.states.async_set("switch.test1", STATE_UNAVAILABLE) hass.states.async_set("switch.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + # All group members unknown -> unknown + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + # Otherwise -> off + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("switch.test1") + hass.states.async_remove("switch.test2") + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, SWITCH_DOMAIN, @@ -110,11 +166,47 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + # At least one member unknown or unavailable -> group unknown hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # At least one member off -> group off hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_OFF) await hass.async_block_till_done() @@ -125,13 +217,15 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_OFF + # Otherwise -> on hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_ON) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_ON - hass.states.async_set("switch.test1", STATE_UNAVAILABLE) - hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("switch.test1") + hass.states.async_remove("switch.test2") await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE