Skip to content

Commit 0cf1fd1

Browse files
emontnemeryfrenck
authored andcommitted
Handle changes to source entity in integration helper (home-assistant#146522)
1 parent 5ee39df commit 0cf1fd1

File tree

2 files changed

+300
-1
lines changed

2 files changed

+300
-1
lines changed

homeassistant/components/integration/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from homeassistant.const import Platform
77
from homeassistant.core import HomeAssistant
88
from homeassistant.helpers.device import (
9+
async_entity_id_to_device_id,
910
async_remove_stale_devices_links_keep_entity_device,
1011
)
12+
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
1113

1214
from .const import CONF_SOURCE_SENSOR
1315

@@ -21,6 +23,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2123
entry.options[CONF_SOURCE_SENSOR],
2224
)
2325

26+
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
27+
hass.config_entries.async_update_entry(
28+
entry,
29+
options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id},
30+
)
31+
32+
async def source_entity_removed() -> None:
33+
# The source entity has been removed, we need to clean the device links.
34+
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
35+
36+
entry.async_on_unload(
37+
async_handle_source_entity_changes(
38+
hass,
39+
helper_config_entry_id=entry.entry_id,
40+
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
41+
source_device_id=async_entity_id_to_device_id(
42+
hass, entry.options[CONF_SOURCE_SENSOR]
43+
),
44+
source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR],
45+
source_entity_removed=source_entity_removed,
46+
)
47+
)
48+
2449
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
2550
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
2651
return True

tests/components/integration/test_init.py

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,97 @@
11
"""Test the Integration - Riemann sum integral integration."""
22

3+
from unittest.mock import patch
4+
35
import pytest
46

7+
from homeassistant.components import integration
8+
from homeassistant.components.integration.config_flow import ConfigFlowHandler
59
from homeassistant.components.integration.const import DOMAIN
6-
from homeassistant.core import HomeAssistant
10+
from homeassistant.config_entries import ConfigEntry
11+
from homeassistant.core import Event, HomeAssistant
712
from homeassistant.helpers import device_registry as dr, entity_registry as er
13+
from homeassistant.helpers.event import async_track_entity_registry_updated_event
814

915
from tests.common import MockConfigEntry
1016

1117

18+
@pytest.fixture
19+
def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry:
20+
"""Fixture to create a sensor config entry."""
21+
sensor_config_entry = MockConfigEntry()
22+
sensor_config_entry.add_to_hass(hass)
23+
return sensor_config_entry
24+
25+
26+
@pytest.fixture
27+
def sensor_device(
28+
device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry
29+
) -> dr.DeviceEntry:
30+
"""Fixture to create a sensor device."""
31+
return device_registry.async_get_or_create(
32+
config_entry_id=sensor_config_entry.entry_id,
33+
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
34+
)
35+
36+
37+
@pytest.fixture
38+
def sensor_entity_entry(
39+
entity_registry: er.EntityRegistry,
40+
sensor_config_entry: ConfigEntry,
41+
sensor_device: dr.DeviceEntry,
42+
) -> er.RegistryEntry:
43+
"""Fixture to create a sensor entity entry."""
44+
return entity_registry.async_get_or_create(
45+
"sensor",
46+
"test",
47+
"unique",
48+
config_entry=sensor_config_entry,
49+
device_id=sensor_device.id,
50+
original_name="ABC",
51+
)
52+
53+
54+
@pytest.fixture
55+
def integration_config_entry(
56+
hass: HomeAssistant,
57+
sensor_entity_entry: er.RegistryEntry,
58+
) -> MockConfigEntry:
59+
"""Fixture to create an integration config entry."""
60+
config_entry = MockConfigEntry(
61+
data={},
62+
domain=DOMAIN,
63+
options={
64+
"method": "trapezoidal",
65+
"name": "My integration",
66+
"round": 1.0,
67+
"source": sensor_entity_entry.entity_id,
68+
"unit_prefix": "k",
69+
"unit_time": "min",
70+
"max_sub_interval": {"minutes": 1},
71+
},
72+
title="My integration",
73+
version=ConfigFlowHandler.VERSION,
74+
minor_version=ConfigFlowHandler.MINOR_VERSION,
75+
)
76+
77+
config_entry.add_to_hass(hass)
78+
79+
return config_entry
80+
81+
82+
def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]:
83+
"""Track entity registry actions for an entity."""
84+
events = []
85+
86+
def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None:
87+
"""Add entity registry updated event to the list."""
88+
events.append(event.data["action"])
89+
90+
async_track_entity_registry_updated_event(hass, entity_id, add_event)
91+
92+
return events
93+
94+
1295
@pytest.mark.parametrize("platform", ["sensor"])
1396
async def test_setup_and_remove_config_entry(
1497
hass: HomeAssistant,
@@ -209,3 +292,194 @@ async def test_device_cleaning(
209292
integration_config_entry.entry_id
210293
)
211294
assert len(devices_after_reload) == 1
295+
296+
297+
async def test_async_handle_source_entity_changes_source_entity_removed(
298+
hass: HomeAssistant,
299+
device_registry: dr.DeviceRegistry,
300+
entity_registry: er.EntityRegistry,
301+
integration_config_entry: MockConfigEntry,
302+
sensor_config_entry: ConfigEntry,
303+
sensor_device: dr.DeviceEntry,
304+
sensor_entity_entry: er.RegistryEntry,
305+
) -> None:
306+
"""Test the integration config entry is removed when the source entity is removed."""
307+
# Add another config entry to the sensor device
308+
other_config_entry = MockConfigEntry()
309+
other_config_entry.add_to_hass(hass)
310+
device_registry.async_update_device(
311+
sensor_device.id, add_config_entry_id=other_config_entry.entry_id
312+
)
313+
314+
assert await hass.config_entries.async_setup(integration_config_entry.entry_id)
315+
await hass.async_block_till_done()
316+
317+
integration_entity_entry = entity_registry.async_get("sensor.my_integration")
318+
assert integration_entity_entry.device_id == sensor_entity_entry.device_id
319+
320+
sensor_device = device_registry.async_get(sensor_device.id)
321+
assert integration_config_entry.entry_id in sensor_device.config_entries
322+
323+
events = track_entity_registry_actions(hass, integration_entity_entry.entity_id)
324+
325+
# Remove the source sensor's config entry from the device, this removes the
326+
# source sensor
327+
with patch(
328+
"homeassistant.components.integration.async_unload_entry",
329+
wraps=integration.async_unload_entry,
330+
) as mock_unload_entry:
331+
device_registry.async_update_device(
332+
sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id
333+
)
334+
await hass.async_block_till_done()
335+
await hass.async_block_till_done()
336+
mock_unload_entry.assert_not_called()
337+
338+
# Check that the integration config entry is removed from the device
339+
sensor_device = device_registry.async_get(sensor_device.id)
340+
assert integration_config_entry.entry_id not in sensor_device.config_entries
341+
342+
# Check that the integration config entry is not removed
343+
assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids()
344+
345+
# Check we got the expected events
346+
assert events == ["update"]
347+
348+
349+
async def test_async_handle_source_entity_changes_source_entity_removed_from_device(
350+
hass: HomeAssistant,
351+
device_registry: dr.DeviceRegistry,
352+
entity_registry: er.EntityRegistry,
353+
integration_config_entry: MockConfigEntry,
354+
sensor_device: dr.DeviceEntry,
355+
sensor_entity_entry: er.RegistryEntry,
356+
) -> None:
357+
"""Test the source entity removed from the source device."""
358+
assert await hass.config_entries.async_setup(integration_config_entry.entry_id)
359+
await hass.async_block_till_done()
360+
361+
integration_entity_entry = entity_registry.async_get("sensor.my_integration")
362+
assert integration_entity_entry.device_id == sensor_entity_entry.device_id
363+
364+
sensor_device = device_registry.async_get(sensor_device.id)
365+
assert integration_config_entry.entry_id in sensor_device.config_entries
366+
367+
events = track_entity_registry_actions(hass, integration_entity_entry.entity_id)
368+
369+
# Remove the source sensor from the device
370+
with patch(
371+
"homeassistant.components.integration.async_unload_entry",
372+
wraps=integration.async_unload_entry,
373+
) as mock_unload_entry:
374+
entity_registry.async_update_entity(
375+
sensor_entity_entry.entity_id, device_id=None
376+
)
377+
await hass.async_block_till_done()
378+
mock_unload_entry.assert_called_once()
379+
380+
# Check that the integration config entry is removed from the device
381+
sensor_device = device_registry.async_get(sensor_device.id)
382+
assert integration_config_entry.entry_id not in sensor_device.config_entries
383+
384+
# Check that the integration config entry is not removed
385+
assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids()
386+
387+
# Check we got the expected events
388+
assert events == ["update"]
389+
390+
391+
async def test_async_handle_source_entity_changes_source_entity_moved_other_device(
392+
hass: HomeAssistant,
393+
device_registry: dr.DeviceRegistry,
394+
entity_registry: er.EntityRegistry,
395+
integration_config_entry: MockConfigEntry,
396+
sensor_config_entry: ConfigEntry,
397+
sensor_device: dr.DeviceEntry,
398+
sensor_entity_entry: er.RegistryEntry,
399+
) -> None:
400+
"""Test the source entity is moved to another device."""
401+
sensor_device_2 = device_registry.async_get_or_create(
402+
config_entry_id=sensor_config_entry.entry_id,
403+
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")},
404+
)
405+
406+
assert await hass.config_entries.async_setup(integration_config_entry.entry_id)
407+
await hass.async_block_till_done()
408+
409+
integration_entity_entry = entity_registry.async_get("sensor.my_integration")
410+
assert integration_entity_entry.device_id == sensor_entity_entry.device_id
411+
412+
sensor_device = device_registry.async_get(sensor_device.id)
413+
assert integration_config_entry.entry_id in sensor_device.config_entries
414+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
415+
assert integration_config_entry.entry_id not in sensor_device_2.config_entries
416+
417+
events = track_entity_registry_actions(hass, integration_entity_entry.entity_id)
418+
419+
# Move the source sensor to another device
420+
with patch(
421+
"homeassistant.components.integration.async_unload_entry",
422+
wraps=integration.async_unload_entry,
423+
) as mock_unload_entry:
424+
entity_registry.async_update_entity(
425+
sensor_entity_entry.entity_id, device_id=sensor_device_2.id
426+
)
427+
await hass.async_block_till_done()
428+
mock_unload_entry.assert_called_once()
429+
430+
# Check that the integration config entry is moved to the other device
431+
sensor_device = device_registry.async_get(sensor_device.id)
432+
assert integration_config_entry.entry_id not in sensor_device.config_entries
433+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
434+
assert integration_config_entry.entry_id in sensor_device_2.config_entries
435+
436+
# Check that the integration config entry is not removed
437+
assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids()
438+
439+
# Check we got the expected events
440+
assert events == ["update"]
441+
442+
443+
async def test_async_handle_source_entity_new_entity_id(
444+
hass: HomeAssistant,
445+
device_registry: dr.DeviceRegistry,
446+
entity_registry: er.EntityRegistry,
447+
integration_config_entry: MockConfigEntry,
448+
sensor_device: dr.DeviceEntry,
449+
sensor_entity_entry: er.RegistryEntry,
450+
) -> None:
451+
"""Test the source entity's entity ID is changed."""
452+
assert await hass.config_entries.async_setup(integration_config_entry.entry_id)
453+
await hass.async_block_till_done()
454+
455+
integration_entity_entry = entity_registry.async_get("sensor.my_integration")
456+
assert integration_entity_entry.device_id == sensor_entity_entry.device_id
457+
458+
sensor_device = device_registry.async_get(sensor_device.id)
459+
assert integration_config_entry.entry_id in sensor_device.config_entries
460+
461+
events = track_entity_registry_actions(hass, integration_entity_entry.entity_id)
462+
463+
# Change the source entity's entity ID
464+
with patch(
465+
"homeassistant.components.integration.async_unload_entry",
466+
wraps=integration.async_unload_entry,
467+
) as mock_unload_entry:
468+
entity_registry.async_update_entity(
469+
sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id"
470+
)
471+
await hass.async_block_till_done()
472+
mock_unload_entry.assert_called_once()
473+
474+
# Check that the integration config entry is updated with the new entity ID
475+
assert integration_config_entry.options["source"] == "sensor.new_entity_id"
476+
477+
# Check that the helper config is still in the device
478+
sensor_device = device_registry.async_get(sensor_device.id)
479+
assert integration_config_entry.entry_id in sensor_device.config_entries
480+
481+
# Check that the integration config entry is not removed
482+
assert integration_config_entry.entry_id in hass.config_entries.async_entry_ids()
483+
484+
# Check we got the expected events
485+
assert events == []

0 commit comments

Comments
 (0)