Skip to content

Commit 9311a87

Browse files
authored
Refactor Sunricher DALI integration to use direct device callbacks (home-assistant#155315)
1 parent b45294d commit 9311a87

File tree

9 files changed

+75
-46
lines changed

9 files changed

+75
-46
lines changed

homeassistant/components/sunricher_dali/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from homeassistant.core import HomeAssistant
1919
from homeassistant.exceptions import ConfigEntryNotReady
2020
from homeassistant.helpers import device_registry as dr
21-
from homeassistant.helpers.dispatcher import async_dispatcher_send
2221

2322
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
2423
from .types import DaliCenterConfigEntry, DaliCenterData
@@ -47,12 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
4746
"You can try to delete the gateway and add it again"
4847
) from exc
4948

50-
def on_online_status(dev_id: str, available: bool) -> None:
51-
signal = f"{DOMAIN}_update_available_{dev_id}"
52-
hass.add_job(async_dispatcher_send, hass, signal, available)
53-
54-
gateway.on_online_status = on_online_status
55-
5649
try:
5750
devices = await gateway.discover_devices()
5851
except DaliGatewayError as exc:

homeassistant/components/sunricher_dali/light.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from typing import Any
77

8-
from PySrDaliGateway import Device
8+
from PySrDaliGateway import CallbackEventType, Device
99
from PySrDaliGateway.helper import is_light_device
1010
from PySrDaliGateway.types import LightStatus
1111

@@ -19,10 +19,6 @@
1919
)
2020
from homeassistant.core import HomeAssistant, callback
2121
from homeassistant.helpers.device_registry import DeviceInfo
22-
from homeassistant.helpers.dispatcher import (
23-
async_dispatcher_connect,
24-
async_dispatcher_send,
25-
)
2622
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2723

2824
from .const import DOMAIN, MANUFACTURER
@@ -40,15 +36,8 @@ async def async_setup_entry(
4036
) -> None:
4137
"""Set up Sunricher DALI light entities from config entry."""
4238
runtime_data = entry.runtime_data
43-
gateway = runtime_data.gateway
4439
devices = runtime_data.devices
4540

46-
def _on_light_status(dev_id: str, status: LightStatus) -> None:
47-
signal = f"{DOMAIN}_update_{dev_id}"
48-
hass.add_job(async_dispatcher_send, hass, signal, status)
49-
50-
gateway.on_light_status = _on_light_status
51-
5241
async_add_entities(
5342
DaliCenterLight(device)
5443
for device in devices
@@ -123,14 +112,16 @@ async def async_turn_off(self, **kwargs: Any) -> None:
123112
async def async_added_to_hass(self) -> None:
124113
"""Handle entity addition to Home Assistant."""
125114

126-
signal = f"{DOMAIN}_update_{self._attr_unique_id}"
127115
self.async_on_remove(
128-
async_dispatcher_connect(self.hass, signal, self._handle_device_update)
116+
self._light.register_listener(
117+
CallbackEventType.LIGHT_STATUS, self._handle_device_update
118+
)
129119
)
130120

131-
signal = f"{DOMAIN}_update_available_{self._attr_unique_id}"
132121
self.async_on_remove(
133-
async_dispatcher_connect(self.hass, signal, self._handle_availability)
122+
self._light.register_listener(
123+
CallbackEventType.ONLINE_STATUS, self._handle_availability
124+
)
134125
)
135126

136127
# read_status() only queues a request on the gateway and relies on the
@@ -187,4 +178,4 @@ def _handle_device_update(self, status: LightStatus) -> None:
187178
):
188179
self._attr_rgbw_color = status["rgbw_color"]
189180

190-
self.async_write_ha_state()
181+
self.schedule_update_ha_state()

homeassistant/components/sunricher_dali/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
77
"iot_class": "local_push",
88
"quality_scale": "bronze",
9-
"requirements": ["PySrDaliGateway==0.13.1"]
9+
"requirements": ["PySrDaliGateway==0.16.2"]
1010
}

homeassistant/components/sunricher_dali/quality_scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ rules:
3535
log-when-unavailable: done
3636
parallel-updates: done
3737
reauthentication-flow: todo
38-
test-coverage: todo
38+
test-coverage: done
3939

4040
# Gold
4141
devices: done

requirements_all.txt

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements_test_all.txt

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
11
"""Tests for the Sunricher Sunricher DALI integration."""
2+
3+
from collections.abc import Callable
4+
from unittest.mock import MagicMock
5+
6+
from PySrDaliGateway import CallbackEventType
7+
8+
9+
def find_device_listener(
10+
device: MagicMock, event_type: CallbackEventType
11+
) -> Callable[..., None]:
12+
"""Find the registered listener callback for a specific device and event type."""
13+
for call in device.register_listener.call_args_list:
14+
if call[0][0] == event_type:
15+
return call[0][1]
16+
raise AssertionError(
17+
f"Listener for event type {event_type} not found on device {device.dev_id}"
18+
)

tests/components/sunricher_dali/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def _create_mock_device(
5656
device.turn_on = MagicMock()
5757
device.turn_off = MagicMock()
5858
device.read_status = MagicMock()
59+
device.register_listener = MagicMock(return_value=lambda: None)
5960
return device
6061

6162

tests/components/sunricher_dali/test_light.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any
44
from unittest.mock import MagicMock, patch
55

6+
from PySrDaliGateway import CallbackEventType
67
import pytest
78

89
from homeassistant.components.light import (
@@ -15,6 +16,8 @@
1516
from homeassistant.core import HomeAssistant
1617
from homeassistant.helpers import device_registry as dr, entity_registry as er
1718

19+
from . import find_device_listener
20+
1821
from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform
1922

2023
TEST_DIMMER_ENTITY_ID = "light.dimmer_0000_02"
@@ -24,13 +27,20 @@
2427
TEST_RGBW_DEVICE_ID = "01040000056A242121110E"
2528

2629

27-
def _dispatch_status(
28-
gateway: MagicMock, device_id: str, status: dict[str, Any]
30+
def _trigger_light_status_callback(
31+
device: MagicMock, device_id: str, status: dict[str, Any]
2932
) -> None:
30-
"""Invoke the status callback registered on the gateway mock."""
31-
callback = gateway.on_light_status
32-
assert callable(callback)
33-
callback(device_id, status)
33+
"""Trigger the light status callbacks registered on the device mock."""
34+
callback = find_device_listener(device, CallbackEventType.LIGHT_STATUS)
35+
callback(status)
36+
37+
38+
def _trigger_availability_callback(
39+
device: MagicMock, device_id: str, available: bool
40+
) -> None:
41+
"""Trigger the availability callbacks registered on the device mock."""
42+
callback = find_device_listener(device, CallbackEventType.ONLINE_STATUS)
43+
callback(available)
3444

3545

3646
@pytest.fixture
@@ -133,26 +143,25 @@ async def test_turn_on_with_brightness(
133143
)
134144

135145

136-
async def test_dispatcher_connection(
146+
async def test_callback_registration(
137147
hass: HomeAssistant,
138148
init_integration: MockConfigEntry,
139149
mock_devices: list[MagicMock],
140-
mock_gateway: MagicMock,
141150
) -> None:
142-
"""Test that dispatcher signals are properly connected."""
143-
entity_entry = er.async_get(hass).async_get(TEST_DIMMER_ENTITY_ID)
144-
assert entity_entry is not None
145-
146-
state = hass.states.get(TEST_DIMMER_ENTITY_ID)
147-
assert state is not None
151+
"""Test that callbacks are properly registered and triggered."""
152+
state_before = hass.states.get(TEST_DIMMER_ENTITY_ID)
153+
assert state_before is not None
148154

149155
status_update: dict[str, Any] = {"is_on": True, "brightness": 128}
150-
151-
_dispatch_status(mock_gateway, TEST_DIMMER_DEVICE_ID, status_update)
156+
_trigger_light_status_callback(
157+
mock_devices[0], TEST_DIMMER_DEVICE_ID, status_update
158+
)
152159
await hass.async_block_till_done()
153160

154161
state_after = hass.states.get(TEST_DIMMER_ENTITY_ID)
155162
assert state_after is not None
163+
assert state_after.state == "on"
164+
assert state_after.attributes.get("brightness") == 128
156165

157166

158167
@pytest.mark.parametrize(
@@ -168,10 +177,28 @@ async def test_dispatcher_connection(
168177
async def test_status_updates(
169178
hass: HomeAssistant,
170179
init_integration: MockConfigEntry,
171-
mock_gateway: MagicMock,
180+
mock_devices: list[MagicMock],
172181
device_id: str,
173182
status_update: dict[str, Any],
174183
) -> None:
175184
"""Test various status updates for different device types."""
176-
_dispatch_status(mock_gateway, device_id, status_update)
185+
device = next(d for d in mock_devices if d.dev_id == device_id)
186+
_trigger_light_status_callback(device, device_id, status_update)
187+
await hass.async_block_till_done()
188+
189+
190+
async def test_device_availability(
191+
hass: HomeAssistant,
192+
init_integration: MockConfigEntry,
193+
mock_devices: list[MagicMock],
194+
) -> None:
195+
"""Test device availability changes."""
196+
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, False)
197+
await hass.async_block_till_done()
198+
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
199+
assert state.state == "unavailable"
200+
201+
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, True)
177202
await hass.async_block_till_done()
203+
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
204+
assert state.state != "unavailable"

0 commit comments

Comments
 (0)