Skip to content

Commit d600abc

Browse files
authored
Base valve platform (#909)
1 parent 0651367 commit d600abc

3 files changed

Lines changed: 264 additions & 0 deletions

File tree

‎tests/test_platform_valve.py‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Test the ZHA valve platform."""
2+
3+
import pytest
4+
from zigpy.profiles import zha
5+
from zigpy.zcl.clusters import general
6+
7+
from tests.common import (
8+
SIG_EP_INPUT,
9+
SIG_EP_OUTPUT,
10+
SIG_EP_PROFILE,
11+
SIG_EP_TYPE,
12+
create_mock_zigpy_device,
13+
join_zigpy_device,
14+
)
15+
from zha.application import Platform
16+
from zha.application.gateway import Gateway
17+
from zha.application.platforms import EntityStateChangedEvent
18+
from zha.application.platforms.valve import BaseValve, ValveEntityState
19+
from zha.application.platforms.valve.const import ValveDeviceClass, ValveEntityFeature
20+
from zha.zigbee.device import Device
21+
22+
23+
class FakeValve(BaseValve):
24+
"""Valve entity that only opens and closes."""
25+
26+
_unique_id_suffix = "fake"
27+
_attr_device_class = ValveDeviceClass.WATER
28+
29+
_closed: bool | None = None
30+
31+
@property
32+
def is_closed(self) -> bool | None:
33+
"""Return if the valve is closed."""
34+
return self._closed
35+
36+
async def async_open_valve(self) -> None:
37+
"""Open the valve."""
38+
self._closed = False
39+
self.maybe_emit_state_changed_event()
40+
41+
async def async_close_valve(self) -> None:
42+
"""Close the valve."""
43+
self._closed = True
44+
self.maybe_emit_state_changed_event()
45+
46+
47+
class FakePositionValve(FakeValve):
48+
"""Valve entity that reports and sets its position."""
49+
50+
_attr_reports_position = True
51+
_attr_supported_features = (
52+
ValveEntityFeature.OPEN
53+
| ValveEntityFeature.CLOSE
54+
| ValveEntityFeature.SET_POSITION
55+
)
56+
57+
_position: int | None = None
58+
59+
@property
60+
def current_valve_position(self) -> int | None:
61+
"""Return the current position of the valve."""
62+
return self._position
63+
64+
async def async_set_valve_position(self, position: int) -> None:
65+
"""Move the valve to a specific position."""
66+
self._position = position
67+
self.maybe_emit_state_changed_event()
68+
69+
70+
@pytest.fixture
71+
async def zha_device(zha_gateway: Gateway) -> Device:
72+
"""Return a joined device to attach valve entities to."""
73+
zigpy_device = create_mock_zigpy_device(
74+
zha_gateway,
75+
{
76+
1: {
77+
SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id],
78+
SIG_EP_OUTPUT: [],
79+
SIG_EP_TYPE: zha.DeviceType.ON_OFF_OUTPUT,
80+
SIG_EP_PROFILE: zha.PROFILE_ID,
81+
}
82+
},
83+
)
84+
return await join_zigpy_device(zha_gateway, zigpy_device)
85+
86+
87+
def create_valve_entity(zha_device: Device, entity_class: type[FakeValve]) -> FakeValve:
88+
"""Create a valve entity on the first endpoint of a device."""
89+
endpoint = zha_device.endpoints[1]
90+
91+
return entity_class(
92+
endpoint=endpoint,
93+
device=zha_device,
94+
cluster=endpoint.zigpy_endpoint.in_clusters[general.OnOff.cluster_id],
95+
)
96+
97+
98+
async def test_valve_state(zha_device: Device) -> None:
99+
"""Test the state of a valve that only opens and closes."""
100+
entity = create_valve_entity(zha_device, FakeValve)
101+
state_changes: list[EntityStateChangedEvent] = []
102+
entity.subscribe_state(state_changes.append)
103+
104+
assert entity.PLATFORM == Platform.VALVE
105+
assert entity.primary_weight == 10
106+
assert entity.device_class == ValveDeviceClass.WATER
107+
assert entity.supported_features == (
108+
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
109+
)
110+
111+
state = entity.state
112+
assert isinstance(state, ValveEntityState)
113+
assert state.device_class == "water"
114+
assert state.reports_position is False
115+
assert state.current_position is None
116+
assert state.is_opening is None
117+
assert state.is_closing is None
118+
assert state.is_closed is None
119+
assert state.supported_features == (
120+
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
121+
)
122+
123+
await entity.async_close_valve()
124+
assert entity.state.is_closed is True
125+
126+
await entity.async_open_valve()
127+
assert entity.state.is_closed is False
128+
assert len(state_changes) == 3
129+
130+
with pytest.raises(NotImplementedError):
131+
await entity.async_set_valve_position(50)
132+
133+
with pytest.raises(NotImplementedError):
134+
await entity.async_stop_valve()
135+
136+
137+
async def test_position_valve_state(zha_device: Device) -> None:
138+
"""Test the state of a valve that reports its position."""
139+
entity = create_valve_entity(zha_device, FakePositionValve)
140+
141+
state = entity.state
142+
assert state.reports_position is True
143+
assert state.current_position is None
144+
assert state.supported_features == (
145+
ValveEntityFeature.OPEN
146+
| ValveEntityFeature.CLOSE
147+
| ValveEntityFeature.SET_POSITION
148+
)
149+
150+
await entity.async_set_valve_position(40)
151+
assert entity.state.current_position == 40
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Valves on Zigbee Home Automation networks."""
2+
3+
from __future__ import annotations
4+
5+
from abc import ABC, abstractmethod
6+
import dataclasses
7+
8+
from zha.application import Platform
9+
from zha.application.platforms import BaseEntityState, PlatformEntity
10+
from zha.application.platforms.valve.const import ValveDeviceClass, ValveEntityFeature
11+
12+
13+
@dataclasses.dataclass(frozen=True, kw_only=True)
14+
class ValveEntityState(BaseEntityState):
15+
"""State for valve entities."""
16+
17+
reports_position: bool
18+
current_position: int | None
19+
is_opening: bool | None
20+
is_closing: bool | None
21+
is_closed: bool | None
22+
supported_features: ValveEntityFeature
23+
24+
25+
class BaseValve(PlatformEntity, ABC):
26+
"""Abstract base class for ZHA valves."""
27+
28+
PLATFORM = Platform.VALVE
29+
30+
_attr_primary_weight = 10
31+
_attr_device_class: ValveDeviceClass | None = None
32+
_attr_reports_position: bool = False
33+
_attr_supported_features: ValveEntityFeature = (
34+
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
35+
)
36+
37+
@property
38+
def state(self) -> ValveEntityState:
39+
"""Return the state of the valve."""
40+
return ValveEntityState(
41+
**super().state.__dict__,
42+
reports_position=self.reports_position,
43+
current_position=self.current_valve_position,
44+
is_opening=self.is_opening,
45+
is_closing=self.is_closing,
46+
is_closed=self.is_closed,
47+
supported_features=self.supported_features,
48+
)
49+
50+
@property
51+
def supported_features(self) -> ValveEntityFeature:
52+
"""Return supported features."""
53+
return self._attr_supported_features
54+
55+
@property
56+
def reports_position(self) -> bool:
57+
"""Return True if the valve reports its position."""
58+
return self._attr_reports_position
59+
60+
@property
61+
def current_valve_position(self) -> int | None:
62+
"""Return the current position of the valve: 0 is closed, 100 is open."""
63+
return None
64+
65+
@property
66+
def is_opening(self) -> bool | None:
67+
"""Return if the valve is opening."""
68+
return None
69+
70+
@property
71+
def is_closing(self) -> bool | None:
72+
"""Return if the valve is closing."""
73+
return None
74+
75+
@property
76+
@abstractmethod
77+
def is_closed(self) -> bool | None:
78+
"""Return if the valve is closed."""
79+
80+
@abstractmethod
81+
async def async_open_valve(self) -> None:
82+
"""Open the valve."""
83+
84+
@abstractmethod
85+
async def async_close_valve(self) -> None:
86+
"""Close the valve."""
87+
88+
async def async_set_valve_position(self, position: int) -> None:
89+
"""Move the valve to a specific position."""
90+
raise NotImplementedError
91+
92+
async def async_stop_valve(self) -> None:
93+
"""Stop the valve."""
94+
raise NotImplementedError
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Constants for the valve platform."""
2+
3+
from enum import IntFlag, StrEnum
4+
5+
6+
class ValveDeviceClass(StrEnum):
7+
"""Device class for valves."""
8+
9+
WATER = "water"
10+
GAS = "gas"
11+
12+
13+
class ValveEntityFeature(IntFlag):
14+
"""Supported features of the valve entity."""
15+
16+
OPEN = 1
17+
CLOSE = 2
18+
SET_POSITION = 4
19+
STOP = 8

0 commit comments

Comments
 (0)