Skip to content

Commit 7ac35c9

Browse files
authored
Explicit entity object discovery, lifetimes, and registration (#365)
* Make entity lifecycle more explicit * Fix tests * WIP: Clean up a bit more * WIP: Add `_remove_callbacks` * WIP: Centralize entity registration * Move over a few more `__init__` event handlers * WIP: Replace `create_platform_entity` with `is_supported` * WIP: Remove device initialization checks and actually use `is_supported` * Initialize the device only after entities are created * WIP * Explicitly register cluster handlers * Test: introduce `Entity.recompute_capabilities()` * Test: migrate a few more entities to `recompute_capabilities` * Only check if an entity is supported after cluster handler initialization * One more `recompute_capabilities` for sensor * Test: add `Entity._always_supported` to properly deal with v2 quirks * Fix remaining failing non-discovery tests * Fix cluster handler edge case not calling `on_add()` (thanks @dmulcahey!) * Clean up definition format * Fix typing * Fix pre-commit issues * Fix remaining tests * Rename `_maybe_create_device_entities` to `_maybe_create_entities` * Fix last failing unit test * Give `get_entity` a strict mode * Fix remaining tests * Drop strict mode by fixing violations * "Fix" final unit test * Implement `endpoint.on_remove` * Move entity creation onto the device object * Make initialization and configuration logic more explicit * Retain the old behavior of implicit initialization during rejoin * Remove `single_device_matches` in favor of explicit entity checks WIP * Clear out unsupported entities in reverse order
1 parent b2b8918 commit 7ac35c9

31 files changed

+939
-744
lines changed

tests/common.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ def get_entity(
231231
qualifier_func: Callable[[BaseEntity], bool] = lambda e: True,
232232
) -> PlatformEntity:
233233
"""Get the first entity of the specified platform on the given device."""
234+
results = []
235+
234236
for entity in device.platform_entities.values():
235237
if platform != entity.PLATFORM:
236238
continue
@@ -247,11 +249,19 @@ def get_entity(
247249
if not qualifier_func(entity):
248250
continue
249251

250-
return entity
252+
results.append(entity)
251253

252-
raise KeyError(
253-
f"No {entity_type} entity found for platform {platform!r} on device {device}: {device.platform_entities}"
254-
)
254+
if len(results) == 0:
255+
raise KeyError(
256+
f"No {entity_type} entity found for platform {platform!r} on device {device}: {device.platform_entities}"
257+
)
258+
259+
if len(results) != 1:
260+
raise KeyError(
261+
f"Multiple {entity_type} entities found for platform {platform!r} on device {device}: {results}"
262+
)
263+
264+
return results[0]
255265

256266

257267
async def group_entity_availability_test(

tests/test_button.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
from zha.application import Platform
3737
from zha.application.gateway import Gateway
3838
from zha.application.platforms import EntityCategory, PlatformEntity
39-
from zha.application.platforms.button import Button, WriteAttributeButton
39+
from zha.application.platforms.button import (
40+
Button,
41+
FrostLockResetButton,
42+
WriteAttributeButton,
43+
)
4044
from zha.application.platforms.button.const import ButtonDeviceClass
4145
from zha.exceptions import ZHAException
4246
from zha.zigbee.device import Device
@@ -136,9 +140,11 @@ async def test_frost_unlock(
136140
cluster = zigpy_device.endpoints[1].tuya_manufacturer
137141
assert cluster is not None
138142
entity: PlatformEntity = get_entity(
139-
zha_device, platform=Platform.BUTTON, entity_type=WriteAttributeButton
143+
zha_device,
144+
platform=Platform.BUTTON,
145+
entity_type=FrostLockResetButton,
140146
)
141-
assert isinstance(entity, WriteAttributeButton)
147+
assert isinstance(entity, FrostLockResetButton)
142148

143149
assert entity._attr_device_class == ButtonDeviceClass.RESTART
144150
assert entity._attr_entity_category == EntityCategory.CONFIG
@@ -246,7 +252,9 @@ async def test_quirks_command_button(
246252
"""Test ZHA button platform."""
247253
zha_device, cluster = await custom_button_device(zha_gateway)
248254
assert cluster is not None
249-
entity: PlatformEntity = get_entity(zha_device, platform=Platform.BUTTON)
255+
entity: PlatformEntity = get_entity(
256+
zha_device, platform=Platform.BUTTON, entity_type=Button
257+
)
250258

251259
with patch(
252260
"zigpy.zcl.Cluster.request",

tests/test_climate.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -290,51 +290,61 @@ async def test_climate_hvac_action_running_state(
290290
dev_climate_sinope, platform=Platform.SENSOR, entity_type=SinopeHVACAction
291291
)
292292

293-
subscriber = MagicMock()
294-
entity.on_event(STATE_CHANGED, subscriber)
295-
sensor_entity.on_event(STATE_CHANGED, subscriber)
293+
subscriber1 = MagicMock()
294+
subscriber2 = MagicMock()
295+
entity.on_event(STATE_CHANGED, subscriber1)
296+
sensor_entity.on_event(STATE_CHANGED, subscriber2)
296297

297298
assert entity.state["hvac_action"] == "off"
298299
assert sensor_entity.state["state"] == "off"
299300

300301
await send_attributes_report(
301302
zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off}
302303
)
304+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
303305
assert entity.state["hvac_action"] == "off"
304306
assert sensor_entity.state["state"] == "off"
307+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 0
305308

306309
await send_attributes_report(
307310
zha_gateway, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto}
308311
)
312+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
309313
assert entity.state["hvac_action"] == "idle"
310314
assert sensor_entity.state["state"] == "idle"
315+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 1
311316

312317
await send_attributes_report(
313318
zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool}
314319
)
320+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
315321
assert entity.state["hvac_action"] == "cooling"
316322
assert sensor_entity.state["state"] == "cooling"
323+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 2
317324

318325
await send_attributes_report(
319326
zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat}
320327
)
328+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
321329
assert entity.state["hvac_action"] == "heating"
322330
assert sensor_entity.state["state"] == "heating"
331+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 3
323332

324333
await send_attributes_report(
325334
zha_gateway, thrm_cluster, {0x001E: Thermostat.RunningMode.Off}
326335
)
336+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
327337
assert entity.state["hvac_action"] == "idle"
328338
assert sensor_entity.state["state"] == "idle"
339+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 4
329340

330341
await send_attributes_report(
331342
zha_gateway, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On}
332343
)
344+
await zha_gateway.async_block_till_done(wait_background_tasks=True)
333345
assert entity.state["hvac_action"] == "fan"
334346
assert sensor_entity.state["state"] == "fan"
335-
336-
# Both entities are updated!
337-
assert len(subscriber.mock_calls) == 2 * 6
347+
assert len(subscriber1.mock_calls) == len(subscriber2.mock_calls) == 5
338348

339349

340350
async def test_sinope_time(

0 commit comments

Comments
 (0)