Skip to content

Commit 4506be5

Browse files
wollewfrenck
andauthored
Complete test coverage for velux light and cover entities (home-assistant#156770)
Co-authored-by: Franck Nijhof <[email protected]>
1 parent 80c611e commit 4506be5

File tree

3 files changed

+287
-8
lines changed

3 files changed

+287
-8
lines changed

homeassistant/components/velux/quality_scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ rules:
3737
reauthentication-flow: todo
3838
test-coverage:
3939
status: todo
40-
comment: cleanup mock_config_entry vs mock_user_config_entry, cleanup mock_pyvlx vs mock_velux_client, remove unused freezer in test_cover_closed, add tests where missing
40+
comment: add tests where missing
4141

4242
# Gold
4343
devices:

tests/components/velux/test_cover.py

Lines changed: 184 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,44 @@
55
import pytest
66
from pyvlx.opening_device import Awning, GarageDoor, Gate, RollerShutter, Window
77

8+
from homeassistant.components.cover import (
9+
ATTR_POSITION,
10+
ATTR_TILT_POSITION,
11+
DOMAIN as COVER_DOMAIN,
12+
SERVICE_CLOSE_COVER,
13+
SERVICE_CLOSE_COVER_TILT,
14+
SERVICE_OPEN_COVER,
15+
SERVICE_OPEN_COVER_TILT,
16+
SERVICE_SET_COVER_POSITION,
17+
SERVICE_SET_COVER_TILT_POSITION,
18+
SERVICE_STOP_COVER,
19+
SERVICE_STOP_COVER_TILT,
20+
)
821
from homeassistant.components.velux import DOMAIN
9-
from homeassistant.const import STATE_CLOSED, STATE_OPEN, Platform
22+
from homeassistant.const import (
23+
STATE_CLOSED,
24+
STATE_CLOSING,
25+
STATE_OPEN,
26+
STATE_OPENING,
27+
Platform,
28+
)
1029
from homeassistant.core import HomeAssistant
1130
from homeassistant.helpers import device_registry as dr, entity_registry as er
1231

1332
from . import update_callback_entity
1433

1534
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
1635

36+
# Apply setup_integration fixture to all tests in this module
37+
pytestmark = pytest.mark.usefixtures("setup_integration")
38+
1739

1840
@pytest.fixture
1941
def platform() -> Platform:
2042
"""Fixture to specify platform to test."""
2143
return Platform.COVER
2244

2345

24-
@pytest.mark.usefixtures("setup_integration")
2546
@pytest.mark.parametrize("mock_pyvlx", ["mock_blind"], indirect=True)
2647
async def test_blind_entity_setup(
2748
hass: HomeAssistant,
@@ -38,7 +59,6 @@ async def test_blind_entity_setup(
3859
)
3960

4061

41-
@pytest.mark.usefixtures("setup_integration")
4262
@pytest.mark.usefixtures("mock_cover_type")
4363
@pytest.mark.parametrize(
4464
"mock_cover_type", [Awning, GarageDoor, Gate, RollerShutter, Window], indirect=True
@@ -63,7 +83,6 @@ async def test_cover_entity_setup(
6383
)
6484

6585

66-
@pytest.mark.usefixtures("setup_integration")
6786
async def test_cover_device_association(
6887
hass: HomeAssistant,
6988
mock_config_entry: MockConfigEntry,
@@ -91,7 +110,6 @@ async def test_cover_device_association(
91110
) in via_device_entry.identifiers
92111

93112

94-
@pytest.mark.usefixtures("setup_integration")
95113
async def test_cover_closed(
96114
hass: HomeAssistant,
97115
mock_window: AsyncMock,
@@ -117,3 +135,164 @@ async def test_cover_closed(
117135
state = hass.states.get(test_entity_id)
118136
assert state is not None
119137
assert state.state == STATE_CLOSED
138+
139+
140+
# Window command tests
141+
142+
143+
async def test_window_open_close_stop_services(
144+
hass: HomeAssistant, mock_window: AsyncMock
145+
) -> None:
146+
"""Verify open/close/stop services map to device calls with no wait."""
147+
148+
entity_id = "cover.test_window"
149+
150+
await hass.services.async_call(
151+
COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True
152+
)
153+
mock_window.open.assert_awaited_once_with(wait_for_completion=False)
154+
155+
await hass.services.async_call(
156+
COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True
157+
)
158+
mock_window.close.assert_awaited_once_with(wait_for_completion=False)
159+
160+
await hass.services.async_call(
161+
COVER_DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True
162+
)
163+
mock_window.stop.assert_awaited_once_with(wait_for_completion=False)
164+
165+
166+
async def test_window_set_cover_position_inversion(
167+
hass: HomeAssistant, mock_window: AsyncMock
168+
) -> None:
169+
"""HA position is inverted for device's Position."""
170+
171+
entity_id = "cover.test_window"
172+
173+
# Call with position 30 (=70% for device)
174+
await hass.services.async_call(
175+
COVER_DOMAIN,
176+
SERVICE_SET_COVER_POSITION,
177+
{"entity_id": entity_id, ATTR_POSITION: 30},
178+
blocking=True,
179+
)
180+
181+
# Expect device Position 70%
182+
args, kwargs = mock_window.set_position.await_args
183+
position_obj = args[0]
184+
assert position_obj.position_percent == 70
185+
assert kwargs.get("wait_for_completion") is False
186+
187+
188+
async def test_window_current_position_and_opening_closing_states(
189+
hass: HomeAssistant, mock_window: AsyncMock
190+
) -> None:
191+
"""Validate current_position and opening/closing state transitions."""
192+
193+
entity_id = "cover.test_window"
194+
195+
# device position 30 -> current_position 70
196+
mock_window.position.position_percent = 30
197+
await update_callback_entity(hass, mock_window)
198+
state = hass.states.get(entity_id)
199+
assert state is not None
200+
assert state.attributes.get("current_position") == 70
201+
assert state.state == STATE_OPEN
202+
203+
# Opening
204+
mock_window.is_opening = True
205+
mock_window.is_closing = False
206+
await update_callback_entity(hass, mock_window)
207+
state = hass.states.get(entity_id)
208+
assert state is not None
209+
assert state.state == STATE_OPENING
210+
211+
# Closing
212+
mock_window.is_opening = False
213+
mock_window.is_closing = True
214+
await update_callback_entity(hass, mock_window)
215+
state = hass.states.get(entity_id)
216+
assert state is not None
217+
assert state.state == STATE_CLOSING
218+
219+
220+
# Blind command tests
221+
222+
223+
async def test_blind_open_close_stop_tilt_services(
224+
hass: HomeAssistant, mock_blind: AsyncMock
225+
) -> None:
226+
"""Verify tilt services map to orientation calls."""
227+
228+
entity_id = "cover.test_blind"
229+
230+
await hass.services.async_call(
231+
COVER_DOMAIN,
232+
SERVICE_OPEN_COVER_TILT,
233+
{"entity_id": entity_id},
234+
blocking=True,
235+
)
236+
mock_blind.open_orientation.assert_awaited_once_with(wait_for_completion=False)
237+
238+
await hass.services.async_call(
239+
COVER_DOMAIN,
240+
SERVICE_CLOSE_COVER_TILT,
241+
{"entity_id": entity_id},
242+
blocking=True,
243+
)
244+
mock_blind.close_orientation.assert_awaited_once_with(wait_for_completion=False)
245+
246+
await hass.services.async_call(
247+
COVER_DOMAIN,
248+
SERVICE_STOP_COVER_TILT,
249+
{"entity_id": entity_id},
250+
blocking=True,
251+
)
252+
mock_blind.stop_orientation.assert_awaited_once_with(wait_for_completion=False)
253+
254+
255+
async def test_blind_set_cover_tilt_position_inversion(
256+
hass: HomeAssistant, mock_blind: AsyncMock
257+
) -> None:
258+
"""HA tilt position is inverted for device orientation."""
259+
260+
entity_id = "cover.test_blind"
261+
262+
await hass.services.async_call(
263+
COVER_DOMAIN,
264+
SERVICE_SET_COVER_TILT_POSITION,
265+
{"entity_id": entity_id, ATTR_TILT_POSITION: 25},
266+
blocking=True,
267+
)
268+
269+
call = mock_blind.set_orientation.await_args
270+
orientation_obj = call.kwargs.get("orientation")
271+
assert orientation_obj is not None
272+
assert orientation_obj.position_percent == 75
273+
assert call.kwargs.get("wait_for_completion") is False
274+
275+
276+
async def test_blind_current_tilt_position(
277+
hass: HomeAssistant, mock_blind: AsyncMock
278+
) -> None:
279+
"""Validate current_tilt_position attribute reflects inverted orientation."""
280+
281+
entity_id = "cover.test_blind"
282+
mock_blind.orientation.position_percent = 10
283+
await update_callback_entity(hass, mock_blind)
284+
state = hass.states.get(entity_id)
285+
assert state is not None
286+
assert state.attributes.get("current_tilt_position") == 90
287+
288+
289+
async def test_non_blind_has_no_tilt_position(
290+
hass: HomeAssistant, mock_window: AsyncMock
291+
) -> None:
292+
"""Non-blind covers should not expose current_tilt_position attribute."""
293+
294+
entity_id = "cover.test_window"
295+
await update_callback_entity(hass, mock_window)
296+
state = hass.states.get(entity_id)
297+
assert state is not None
298+
assert "current_tilt_position" not in state.attributes

tests/components/velux/test_light.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,30 @@
44

55
import pytest
66

7+
from homeassistant.components.light import (
8+
ATTR_BRIGHTNESS,
9+
DOMAIN as LIGHT_DOMAIN,
10+
SERVICE_TURN_OFF,
11+
SERVICE_TURN_ON,
12+
)
713
from homeassistant.const import Platform
814
from homeassistant.core import HomeAssistant
915
from homeassistant.helpers import device_registry as dr, entity_registry as er
1016

17+
from . import update_callback_entity
18+
1119
from tests.common import MockConfigEntry
1220

21+
# Apply setup_integration fixture to all tests in this module
22+
pytestmark = pytest.mark.usefixtures("setup_integration")
23+
1324

1425
@pytest.fixture
1526
def platform() -> Platform:
1627
"""Fixture to specify platform to test."""
1728
return Platform.LIGHT
1829

1930

20-
@pytest.mark.usefixtures("setup_integration")
2131
async def test_light_setup(
2232
hass: HomeAssistant,
2333
entity_registry: er.EntityRegistry,
@@ -46,7 +56,6 @@ async def test_light_setup(
4656

4757

4858
# This test is not light specific, it just uses the light platform to test the base entity class.
49-
@pytest.mark.usefixtures("setup_integration")
5059
async def test_entity_callbacks(
5160
hass: HomeAssistant,
5261
mock_config_entry: MockConfigEntry,
@@ -70,3 +79,94 @@ async def test_entity_callbacks(
7079
# Callback must be unregistered with the same callable
7180
assert mock_light.unregister_device_updated_cb.call_count == 1
7281
assert mock_light.unregister_device_updated_cb.call_args[0][0] is cb
82+
83+
84+
async def test_light_brightness_and_is_on(
85+
hass: HomeAssistant, mock_light: AsyncMock
86+
) -> None:
87+
"""Validate brightness mapping and on/off state from intensity."""
88+
89+
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
90+
91+
# Set initial intensity values
92+
mock_light.intensity.intensity_percent = 20 # 20% "intensity" -> 80% brightness
93+
mock_light.intensity.off = False
94+
mock_light.intensity.known = True
95+
96+
# Trigger state write
97+
await update_callback_entity(hass, mock_light)
98+
99+
state = hass.states.get(entity_id)
100+
assert state is not None
101+
# brightness = int((100 - 20) * 255 / 100) = int(204)
102+
assert state.attributes.get("brightness") == 204
103+
assert state.state == "on"
104+
105+
# Mark as off
106+
mock_light.intensity.off = True
107+
await update_callback_entity(hass, mock_light)
108+
state = hass.states.get(entity_id)
109+
assert state is not None
110+
assert state.state == "off"
111+
112+
113+
async def test_light_turn_on_with_brightness_uses_set_intensity(
114+
hass: HomeAssistant, mock_light: AsyncMock
115+
) -> None:
116+
"""Turning on with brightness calls set_intensity with inverted percent."""
117+
118+
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
119+
120+
# Call turn_on with brightness=51 (20% when normalized)
121+
await hass.services.async_call(
122+
LIGHT_DOMAIN,
123+
SERVICE_TURN_ON,
124+
{"entity_id": entity_id, ATTR_BRIGHTNESS: 51},
125+
blocking=True,
126+
)
127+
128+
# set_intensity called once; turn_on should not be used in this path
129+
assert mock_light.set_intensity.await_count == 1
130+
assert mock_light.turn_on.await_count == 0
131+
132+
# Inspect the intensity argument (first positional)
133+
args, kwargs = mock_light.set_intensity.await_args
134+
intensity_obj = args[0]
135+
# brightness 51 -> 20% normalized -> intensity_percent = 80
136+
assert intensity_obj.intensity_percent == 80
137+
assert kwargs.get("wait_for_completion") is True
138+
139+
140+
async def test_light_turn_on_without_brightness_calls_turn_on(
141+
hass: HomeAssistant, mock_light: AsyncMock
142+
) -> None:
143+
"""Turning on without brightness uses device.turn_on."""
144+
145+
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
146+
147+
await hass.services.async_call(
148+
LIGHT_DOMAIN,
149+
SERVICE_TURN_ON,
150+
{"entity_id": entity_id},
151+
blocking=True,
152+
)
153+
154+
mock_light.turn_on.assert_awaited_once_with(wait_for_completion=True)
155+
assert mock_light.set_intensity.await_count == 0
156+
157+
158+
async def test_light_turn_off_calls_turn_off(
159+
hass: HomeAssistant, mock_light: AsyncMock
160+
) -> None:
161+
"""Turning off calls device.turn_off with wait_for_completion."""
162+
163+
entity_id = f"light.{mock_light.name.lower().replace(' ', '_')}"
164+
165+
await hass.services.async_call(
166+
LIGHT_DOMAIN,
167+
SERVICE_TURN_OFF,
168+
{"entity_id": entity_id},
169+
blocking=True,
170+
)
171+
172+
mock_light.turn_off.assert_awaited_once_with(wait_for_completion=True)

0 commit comments

Comments
 (0)