Skip to content

Commit d108d5f

Browse files
thecodeCopilot
andauthored
Use Shelly RPC cover methods from upstream and fix cover status update (home-assistant#154345)
Co-authored-by: Copilot <[email protected]>
1 parent 4879408 commit d108d5f

File tree

3 files changed

+71
-121
lines changed

3 files changed

+71
-121
lines changed

homeassistant/components/shelly/cover.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
ShellyRpcAttributeEntity,
3030
async_setup_entry_attribute_entities,
3131
async_setup_entry_rpc,
32+
rpc_call,
3233
)
3334
from .utils import get_device_entry_gen
3435

@@ -192,6 +193,7 @@ class RpcShellyCover(ShellyRpcAttributeEntity, CoverEntity):
192193
_attr_supported_features: CoverEntityFeature = (
193194
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
194195
)
196+
_id: int
195197

196198
def __init__(
197199
self,
@@ -260,7 +262,7 @@ async def update_position(self) -> None:
260262
"""Update the cover position every second."""
261263
try:
262264
while self.is_closing or self.is_opening:
263-
await self.coordinator.device.update_status()
265+
await self.coordinator.device.update_cover_status(self._id)
264266
self.async_write_ha_state()
265267
await asyncio.sleep(RPC_COVER_UPDATE_TIME_SEC)
266268
finally:
@@ -274,39 +276,46 @@ def _update_callback(self) -> None:
274276
if self.is_closing or self.is_opening:
275277
self.launch_update_task()
276278

279+
@rpc_call
277280
async def async_close_cover(self, **kwargs: Any) -> None:
278281
"""Close cover."""
279-
await self.call_rpc("Cover.Close", {"id": self._id})
282+
await self.coordinator.device.cover_close(self._id)
280283

284+
@rpc_call
281285
async def async_open_cover(self, **kwargs: Any) -> None:
282286
"""Open cover."""
283-
await self.call_rpc("Cover.Open", {"id": self._id})
287+
await self.coordinator.device.cover_open(self._id)
284288

289+
@rpc_call
285290
async def async_set_cover_position(self, **kwargs: Any) -> None:
286291
"""Move the cover to a specific position."""
287-
await self.call_rpc(
288-
"Cover.GoToPosition", {"id": self._id, "pos": kwargs[ATTR_POSITION]}
292+
await self.coordinator.device.cover_set_position(
293+
self._id, pos=kwargs[ATTR_POSITION]
289294
)
290295

296+
@rpc_call
291297
async def async_stop_cover(self, **_kwargs: Any) -> None:
292298
"""Stop the cover."""
293-
await self.call_rpc("Cover.Stop", {"id": self._id})
299+
await self.coordinator.device.cover_stop(self._id)
294300

301+
@rpc_call
295302
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
296303
"""Open the cover tilt."""
297-
await self.call_rpc("Cover.GoToPosition", {"id": self._id, "slat_pos": 100})
304+
await self.coordinator.device.cover_set_position(self._id, slat_pos=100)
298305

306+
@rpc_call
299307
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
300308
"""Close the cover tilt."""
301-
await self.call_rpc("Cover.GoToPosition", {"id": self._id, "slat_pos": 0})
309+
await self.coordinator.device.cover_set_position(self._id, slat_pos=0)
302310

311+
@rpc_call
303312
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
304313
"""Move the cover tilt to a specific position."""
305-
await self.call_rpc(
306-
"Cover.GoToPosition",
307-
{"id": self._id, "slat_pos": kwargs[ATTR_TILT_POSITION]},
314+
await self.coordinator.device.cover_set_position(
315+
self._id, slat_pos=kwargs[ATTR_TILT_POSITION]
308316
)
309317

318+
@rpc_call
310319
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
311320
"""Stop the cover."""
312-
await self.call_rpc("Cover.Stop", {"id": self._id})
321+
await self.coordinator.device.cover_stop(self._id)

tests/components/shelly/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,13 +617,23 @@ def initialized():
617617
{}, RpcUpdateType.INITIALIZED
618618
)
619619

620+
current_pos = iter(range(50, -1, -10)) # from 50 to 0 in steps of 10
621+
622+
async def update_cover_status(cover_id: int):
623+
device.status[f"cover:{cover_id}"]["current_pos"] = next(
624+
current_pos, device.status[f"cover:{cover_id}"]["current_pos"]
625+
)
626+
620627
device = _mock_rpc_device()
621628
rpc_device_mock.return_value = device
622629
rpc_device_mock.return_value.mock_disconnected = Mock(side_effect=disconnected)
623630
rpc_device_mock.return_value.mock_update = Mock(side_effect=update)
624631
rpc_device_mock.return_value.mock_event = Mock(side_effect=event)
625632
rpc_device_mock.return_value.mock_online = Mock(side_effect=online)
626633
rpc_device_mock.return_value.mock_initialized = Mock(side_effect=initialized)
634+
rpc_device_mock.return_value.update_cover_status = AsyncMock(
635+
side_effect=update_cover_status
636+
)
627637

628638
yield rpc_device_mock.return_value
629639

tests/components/shelly/test_cover.py

Lines changed: 40 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ async def test_rpc_device_services(
139139
{ATTR_ENTITY_ID: entity_id, ATTR_POSITION: 50},
140140
blocking=True,
141141
)
142+
143+
mock_rpc_device.cover_set_position.assert_called_once_with(0, pos=50)
142144
assert (state := hass.states.get(entity_id))
143145
assert state.attributes[ATTR_CURRENT_POSITION] == 50
144146

@@ -153,6 +155,7 @@ async def test_rpc_device_services(
153155
)
154156
mock_rpc_device.mock_update()
155157

158+
mock_rpc_device.cover_open.assert_called_once_with(0)
156159
assert (state := hass.states.get(entity_id))
157160
assert state.state == CoverState.OPENING
158161

@@ -167,6 +170,7 @@ async def test_rpc_device_services(
167170
)
168171
mock_rpc_device.mock_update()
169172

173+
mock_rpc_device.cover_close.assert_called_once_with(0)
170174
assert (state := hass.states.get(entity_id))
171175
assert state.state == CoverState.CLOSING
172176

@@ -178,6 +182,8 @@ async def test_rpc_device_services(
178182
blocking=True,
179183
)
180184
mock_rpc_device.mock_update()
185+
186+
mock_rpc_device.cover_stop.assert_called_once_with(0)
181187
assert (state := hass.states.get(entity_id))
182188
assert state.state == CoverState.CLOSED
183189

@@ -262,9 +268,11 @@ async def test_rpc_cover_tilt(
262268
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "slat_pos", 50)
263269
mock_rpc_device.mock_update()
264270

271+
mock_rpc_device.cover_set_position.assert_called_once_with(0, slat_pos=50)
265272
assert (state := hass.states.get(entity_id))
266273
assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 50
267274

275+
mock_rpc_device.cover_set_position.reset_mock()
268276
await hass.services.async_call(
269277
COVER_DOMAIN,
270278
SERVICE_OPEN_COVER_TILT,
@@ -274,9 +282,11 @@ async def test_rpc_cover_tilt(
274282
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "slat_pos", 100)
275283
mock_rpc_device.mock_update()
276284

285+
mock_rpc_device.cover_set_position.assert_called_once_with(0, slat_pos=100)
277286
assert (state := hass.states.get(entity_id))
278287
assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 100
279288

289+
mock_rpc_device.cover_set_position.reset_mock()
280290
await hass.services.async_call(
281291
COVER_DOMAIN,
282292
SERVICE_CLOSE_COVER_TILT,
@@ -292,157 +302,78 @@ async def test_rpc_cover_tilt(
292302
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "slat_pos", 10)
293303
mock_rpc_device.mock_update()
294304

305+
mock_rpc_device.cover_stop.assert_called_once_with(0)
295306
assert (state := hass.states.get(entity_id))
296307
assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 10
297308

298309

299-
async def test_update_position_closing(
310+
async def test_rpc_cover_position_update(
300311
hass: HomeAssistant,
301312
freezer: FrozenDateTimeFactory,
302313
mock_rpc_device: Mock,
303314
monkeypatch: pytest.MonkeyPatch,
304315
) -> None:
305-
"""Test update_position while the cover is closing."""
316+
"""Test RPC update_position while the cover is moving."""
306317
entity_id = "cover.test_name_test_cover_0"
307318
await init_integration(hass, 2)
308319

309-
# Set initial state to closing
320+
# Set initial state to closing, position 50 set by update_cover_status mock
310321
mutate_rpc_device_status(
311322
monkeypatch, mock_rpc_device, "cover:0", "state", "closing"
312323
)
313-
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "current_pos", 40)
314324
mock_rpc_device.mock_update()
315325

316326
assert (state := hass.states.get(entity_id))
317327
assert state.state == CoverState.CLOSING
318-
assert state.attributes[ATTR_CURRENT_POSITION] == 40
319-
320-
# Simulate position decrement
321-
async def simulated_update(*args, **kwargs):
322-
pos = mock_rpc_device.status["cover:0"]["current_pos"]
323-
if pos > 0:
324-
mutate_rpc_device_status(
325-
monkeypatch, mock_rpc_device, "cover:0", "current_pos", pos - 10
326-
)
327-
else:
328-
mutate_rpc_device_status(
329-
monkeypatch, mock_rpc_device, "cover:0", "current_pos", 0
330-
)
331-
mutate_rpc_device_status(
332-
monkeypatch, mock_rpc_device, "cover:0", "state", "closed"
333-
)
334-
335-
# Patching the mock update_status method
336-
monkeypatch.setattr(mock_rpc_device, "update_status", simulated_update)
328+
assert state.attributes[ATTR_CURRENT_POSITION] == 50
337329

338330
# Simulate position updates during closing
339331
for position in range(40, -1, -10):
340-
assert (state := hass.states.get(entity_id))
341-
assert state.attributes[ATTR_CURRENT_POSITION] == position
342-
assert state.state == CoverState.CLOSING
332+
mock_rpc_device.update_cover_status.reset_mock()
343333
await mock_polling_rpc_update(hass, freezer, RPC_COVER_UPDATE_TIME_SEC)
344334

345-
# Final state should be closed
346-
assert (state := hass.states.get(entity_id))
347-
assert state.state == CoverState.CLOSED
348-
assert state.attributes[ATTR_CURRENT_POSITION] == 0
349-
350-
351-
async def test_update_position_opening(
352-
hass: HomeAssistant,
353-
freezer: FrozenDateTimeFactory,
354-
mock_rpc_device: Mock,
355-
monkeypatch: pytest.MonkeyPatch,
356-
) -> None:
357-
"""Test update_position while the cover is opening."""
358-
entity_id = "cover.test_name_test_cover_0"
359-
await init_integration(hass, 2)
360-
361-
# Set initial state to opening at 60
362-
mutate_rpc_device_status(
363-
monkeypatch, mock_rpc_device, "cover:0", "state", "opening"
364-
)
365-
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "current_pos", 60)
366-
mock_rpc_device.mock_update()
367-
368-
assert (state := hass.states.get(entity_id))
369-
assert state.state == CoverState.OPENING
370-
assert state.attributes[ATTR_CURRENT_POSITION] == 60
371-
372-
# Simulate position increment
373-
async def simulated_update(*args, **kwargs):
374-
pos = mock_rpc_device.status["cover:0"]["current_pos"]
375-
if pos < 100:
376-
mutate_rpc_device_status(
377-
monkeypatch, mock_rpc_device, "cover:0", "current_pos", pos + 10
378-
)
379-
else:
380-
mutate_rpc_device_status(
381-
monkeypatch, mock_rpc_device, "cover:0", "current_pos", 100
382-
)
383-
mutate_rpc_device_status(
384-
monkeypatch, mock_rpc_device, "cover:0", "state", "open"
385-
)
386-
387-
# Patching the mock update_status method
388-
monkeypatch.setattr(mock_rpc_device, "update_status", simulated_update)
389-
390-
# Check position updates during opening
391-
for position in range(60, 101, 10):
335+
mock_rpc_device.update_cover_status.assert_called_once_with(0)
392336
assert (state := hass.states.get(entity_id))
393337
assert state.attributes[ATTR_CURRENT_POSITION] == position
394-
assert state.state == CoverState.OPENING
395-
await mock_polling_rpc_update(hass, freezer, RPC_COVER_UPDATE_TIME_SEC)
396-
397-
# Final state should be open
398-
assert (state := hass.states.get(entity_id))
399-
assert state.state == CoverState.OPEN
400-
assert state.attributes[ATTR_CURRENT_POSITION] == 100
401-
402-
403-
async def test_update_position_no_movement(
404-
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
405-
) -> None:
406-
"""Test update_position when the cover is not moving."""
407-
entity_id = "cover.test_name_test_cover_0"
408-
await init_integration(hass, 2)
338+
assert state.state == CoverState.CLOSING
409339

410-
# Set initial state to open
411-
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "open")
412-
mutate_rpc_device_status(
413-
monkeypatch, mock_rpc_device, "cover:0", "current_pos", 100
414-
)
340+
# Simulate cover reaching final position
341+
mock_rpc_device.update_cover_status.reset_mock()
342+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "state", "closed")
415343
mock_rpc_device.mock_update()
416344

417345
assert (state := hass.states.get(entity_id))
418-
assert state.state == CoverState.OPEN
419-
assert state.attributes[ATTR_CURRENT_POSITION] == 100
420-
421-
# Call update_position and ensure no changes occur
422-
await hass.services.async_call(
423-
COVER_DOMAIN,
424-
SERVICE_OPEN_COVER,
425-
{ATTR_ENTITY_ID: entity_id},
426-
blocking=True,
427-
)
346+
assert state.attributes[ATTR_CURRENT_POSITION] == 0
347+
assert state.state == CoverState.CLOSED
428348

429-
assert (state := hass.states.get(entity_id))
430-
assert state.state == CoverState.OPEN
431-
assert state.attributes[ATTR_CURRENT_POSITION] == 100
349+
# Ensure update_position does not call update_cover_status when the cover is not moving
350+
await mock_polling_rpc_update(hass, freezer, RPC_COVER_UPDATE_TIME_SEC)
351+
mock_rpc_device.update_cover_status.assert_not_called()
432352

433353

434354
async def test_rpc_not_initialized_update(
435-
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
355+
hass: HomeAssistant,
356+
mock_rpc_device: Mock,
357+
monkeypatch: pytest.MonkeyPatch,
358+
freezer: FrozenDateTimeFactory,
436359
) -> None:
437360
"""Test update not called when device is not initialized."""
438361
entity_id = "cover.test_name_test_cover_0"
439362
await init_integration(hass, 2)
440363

441-
assert (state := hass.states.get(entity_id))
442-
assert state.state == CoverState.OPEN
364+
# Set initial state to closing
365+
mutate_rpc_device_status(
366+
monkeypatch, mock_rpc_device, "cover:0", "state", "closing"
367+
)
368+
mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "current_pos", 40)
443369

370+
# mock device not initialized (e.g. disconnected)
444371
monkeypatch.setattr(mock_rpc_device, "initialized", False)
445372
mock_rpc_device.mock_update()
446373

374+
# wait for update interval to allow update_position to call update_cover_status
375+
await mock_polling_rpc_update(hass, freezer, RPC_COVER_UPDATE_TIME_SEC)
376+
377+
mock_rpc_device.update_cover_status.assert_not_called()
447378
assert (state := hass.states.get(entity_id))
448379
assert state.state == STATE_UNAVAILABLE

0 commit comments

Comments
 (0)