mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
Add is_closed state attribute to valve (#165227)
This commit is contained in:
@@ -62,6 +62,7 @@ class ValveEntityFeature(IntFlag):
|
||||
|
||||
|
||||
ATTR_CURRENT_POSITION = "current_position"
|
||||
ATTR_IS_CLOSED = "is_closed"
|
||||
ATTR_POSITION = "position"
|
||||
|
||||
|
||||
@@ -189,11 +190,20 @@ class ValveEntity(Entity):
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any] | None:
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
if not self.reports_position:
|
||||
return None
|
||||
return {ATTR_CURRENT_POSITION: self.current_valve_position}
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
if self.reports_position:
|
||||
if (current_valve_position := self.current_valve_position) is None:
|
||||
data[ATTR_IS_CLOSED] = None
|
||||
else:
|
||||
data[ATTR_IS_CLOSED] = current_valve_position == 0
|
||||
data[ATTR_CURRENT_POSITION] = current_valve_position
|
||||
else:
|
||||
data[ATTR_IS_CLOSED] = self.is_closed
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ValveEntityFeature:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Mock Title',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -19,6 +20,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Mock Title',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -491,7 +491,7 @@ LOCK_ATTRS = [{"supported_features": 1}, {}]
|
||||
NOTIFY_ATTRS = [{"supported_features": 0}, {}]
|
||||
MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}]
|
||||
SENSOR_ATTRS = [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}]
|
||||
VALVE_ATTRS = [{"supported_features": 0}, {}]
|
||||
VALVE_ATTRS = [{"supported_features": 0}, {"is_closed": False}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
'current_position': 0,
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Test Valve Valve position',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 4>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
'attribution': 'Data provided by hydrawise.com',
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Zone One',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -95,6 +96,7 @@
|
||||
'attribution': 'Data provided by hydrawise.com',
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Zone Two',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Mock Valve',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'volvo',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'balkonbewässerung Valve',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -93,6 +94,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Garden Valve Yard Valve',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -145,6 +147,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 1',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -197,6 +200,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 2',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -249,6 +253,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 3',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -301,6 +306,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 4',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -353,6 +359,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 5',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -405,6 +412,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 6',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -457,6 +465,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 7',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -509,6 +518,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': '接HA水阀 Valve 8',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -613,6 +623,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Sprinkler Cesare Valve',
|
||||
'is_closed': True,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Valve',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@@ -18,6 +19,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 50,
|
||||
'friendly_name': 'Valve',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
@@ -6,6 +6,8 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.valve import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_IS_CLOSED,
|
||||
DOMAIN,
|
||||
ValveDeviceClass,
|
||||
ValveEntity,
|
||||
@@ -234,7 +236,7 @@ async def test_services(
|
||||
|
||||
# Test init all valves should be open
|
||||
assert is_open(hass, ent1)
|
||||
assert is_open(hass, ent2)
|
||||
assert is_open(hass, ent2, 50)
|
||||
|
||||
# call basic toggle services
|
||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||
@@ -243,9 +245,9 @@ async def test_services(
|
||||
|
||||
# entities without stop should be closed and with stop should be closing
|
||||
assert is_closed(hass, ent1)
|
||||
assert is_closing(hass, ent2)
|
||||
assert is_closing(hass, ent2, 50)
|
||||
ent2.finish_movement()
|
||||
assert is_closed(hass, ent2)
|
||||
assert is_closed(hass, ent2, 0)
|
||||
|
||||
# call basic toggle services and set different valve position states
|
||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||
@@ -254,7 +256,9 @@ async def test_services(
|
||||
|
||||
# entities should be in correct state depending on the SUPPORT_STOP feature and valve position
|
||||
assert is_open(hass, ent1)
|
||||
assert is_opening(hass, ent2)
|
||||
assert is_opening(hass, ent2, 0, True)
|
||||
ent2.finish_movement()
|
||||
assert is_open(hass, ent2, 100)
|
||||
|
||||
# call basic toggle services
|
||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||
@@ -264,12 +268,16 @@ async def test_services(
|
||||
# entities should be in correct state depending on the SUPPORT_STOP feature and valve position
|
||||
assert is_closed(hass, ent1)
|
||||
assert not is_opening(hass, ent2)
|
||||
assert not is_closing(hass, ent2)
|
||||
assert is_closed(hass, ent2)
|
||||
assert not is_closed(hass, ent2, 100)
|
||||
assert is_closing(hass, ent2, 100)
|
||||
ent2.finish_movement()
|
||||
assert is_closed(hass, ent2, 0)
|
||||
|
||||
await call_service(hass, SERVICE_SET_VALVE_POSITION, ent2, 50)
|
||||
await hass.async_block_till_done()
|
||||
assert is_opening(hass, ent2)
|
||||
assert is_opening(hass, ent2, 0, True)
|
||||
ent2.finish_movement()
|
||||
assert is_open(hass, ent2, 50)
|
||||
|
||||
|
||||
async def test_valve_device_class(hass: HomeAssistant) -> None:
|
||||
@@ -324,6 +332,45 @@ async def test_none_state(hass: HomeAssistant) -> None:
|
||||
assert pos_valve_with_none_is_closed_attr.state is None
|
||||
|
||||
|
||||
async def test_is_closed_state_attribute(hass: HomeAssistant) -> None:
|
||||
"""Test the behavior of the is_closed state attribute."""
|
||||
binary_valve = MockBinaryValveEntity()
|
||||
binary_valve.hass = hass
|
||||
|
||||
assert binary_valve.state_attributes[ATTR_IS_CLOSED] is None
|
||||
|
||||
binary_valve._attr_is_closed = True
|
||||
assert binary_valve.state_attributes[ATTR_IS_CLOSED] is True
|
||||
|
||||
binary_valve._attr_is_closed = False
|
||||
assert binary_valve.state_attributes[ATTR_IS_CLOSED] is False
|
||||
|
||||
pos_valve = MockValveEntity()
|
||||
pos_valve.hass = hass
|
||||
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is None
|
||||
|
||||
# is_closed property is ignored for position reporting valves,
|
||||
# so it should remain None even if set to True
|
||||
pos_valve._attr_is_closed = True
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is None
|
||||
|
||||
# if current position is 0, the valve is closed
|
||||
pos_valve._attr_current_valve_position = 0
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is True
|
||||
|
||||
# if current position is greater than 0, the valve is not closed
|
||||
pos_valve._attr_current_valve_position = 1
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is False
|
||||
pos_valve._attr_current_valve_position = 50
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is False
|
||||
|
||||
# if current position is None, the valve close state attribute
|
||||
# is unknown and no fallback to the is_closed property is done
|
||||
pos_valve._attr_current_valve_position = None
|
||||
assert pos_valve.state_attributes[ATTR_IS_CLOSED] is None
|
||||
|
||||
|
||||
async def test_supported_features(hass: HomeAssistant) -> None:
|
||||
"""Test valve entity with defaults."""
|
||||
valve = MockValveEntity(features=None)
|
||||
@@ -347,21 +394,69 @@ def set_valve_position(ent, position) -> None:
|
||||
ent._values["current_valve_position"] = position
|
||||
|
||||
|
||||
def is_open(hass: HomeAssistant, ent: ValveEntity) -> bool:
|
||||
def _check_state(
|
||||
hass: HomeAssistant,
|
||||
ent: ValveEntity,
|
||||
expected_state: str,
|
||||
expected_position: int | None,
|
||||
expected_is_closed: bool,
|
||||
) -> bool:
|
||||
"""Check if the state of a valve is as expected."""
|
||||
state = hass.states.get(ent.entity_id)
|
||||
correct_state = state.state == expected_state
|
||||
correct_is_closed = state.attributes.get(ATTR_IS_CLOSED) == expected_is_closed
|
||||
correct_position = state.attributes.get(ATTR_CURRENT_POSITION) == expected_position
|
||||
return all([correct_state, correct_is_closed, correct_position])
|
||||
|
||||
|
||||
def is_open(hass: HomeAssistant, ent: ValveEntity, position: int | None = None) -> bool:
|
||||
"""Return if the valve is open based on the statemachine."""
|
||||
return _check_state(
|
||||
hass,
|
||||
ent,
|
||||
expected_state=ValveState.OPEN,
|
||||
expected_position=position,
|
||||
expected_is_closed=False,
|
||||
)
|
||||
|
||||
|
||||
def is_opening(
|
||||
hass: HomeAssistant,
|
||||
ent: ValveEntity,
|
||||
position: int | None = None,
|
||||
closed: bool = False,
|
||||
) -> bool:
|
||||
"""Return if the valve is opening based on the statemachine."""
|
||||
return _check_state(
|
||||
hass,
|
||||
ent,
|
||||
expected_state=ValveState.OPENING,
|
||||
expected_position=position,
|
||||
expected_is_closed=closed,
|
||||
)
|
||||
|
||||
|
||||
def is_closed(
|
||||
hass: HomeAssistant, ent: ValveEntity, position: int | None = None
|
||||
) -> bool:
|
||||
"""Return if the valve is closed based on the statemachine."""
|
||||
return hass.states.is_state(ent.entity_id, ValveState.OPEN)
|
||||
return _check_state(
|
||||
hass,
|
||||
ent,
|
||||
expected_state=ValveState.CLOSED,
|
||||
expected_position=position,
|
||||
expected_is_closed=True,
|
||||
)
|
||||
|
||||
|
||||
def is_opening(hass: HomeAssistant, ent: ValveEntity) -> bool:
|
||||
"""Return if the valve is closed based on the statemachine."""
|
||||
return hass.states.is_state(ent.entity_id, ValveState.OPENING)
|
||||
|
||||
|
||||
def is_closed(hass: HomeAssistant, ent: ValveEntity) -> bool:
|
||||
"""Return if the valve is closed based on the statemachine."""
|
||||
return hass.states.is_state(ent.entity_id, ValveState.CLOSED)
|
||||
|
||||
|
||||
def is_closing(hass: HomeAssistant, ent: ValveEntity) -> bool:
|
||||
"""Return if the valve is closed based on the statemachine."""
|
||||
return hass.states.is_state(ent.entity_id, ValveState.CLOSING)
|
||||
def is_closing(
|
||||
hass: HomeAssistant, ent: ValveEntity, position: int | None = None
|
||||
) -> bool:
|
||||
"""Return if the valve is closing based on the statemachine."""
|
||||
return _check_state(
|
||||
hass,
|
||||
ent,
|
||||
expected_state=ValveState.CLOSING,
|
||||
expected_position=position,
|
||||
expected_is_closed=False,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'water',
|
||||
'friendly_name': 'Sonic',
|
||||
'is_closed': False,
|
||||
'supported_features': <ValveEntityFeature: 3>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
||||
Reference in New Issue
Block a user