Skip to content

Commit 273ccb3

Browse files
emontnemeryfrenck
authored andcommitted
Handle changes to source entity in trend helper (home-assistant#146525)
1 parent caaa4d5 commit 273ccb3

File tree

2 files changed

+299
-2
lines changed

2 files changed

+299
-2
lines changed

homeassistant/components/trend/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from homeassistant.const import CONF_ENTITY_ID, 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
PLATFORMS = [Platform.BINARY_SENSOR]
1315

@@ -21,6 +23,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2123
entry.options[CONF_ENTITY_ID],
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_ENTITY_ID: source_entity_id},
30+
)
31+
32+
async def source_entity_removed() -> None:
33+
# The source entity has been removed, we remove the config entry because
34+
# trend does not allow replacing the input entity.
35+
await hass.config_entries.async_remove(entry.entry_id)
36+
37+
entry.async_on_unload(
38+
async_handle_source_entity_changes(
39+
hass,
40+
helper_config_entry_id=entry.entry_id,
41+
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
42+
source_device_id=async_entity_id_to_device_id(
43+
hass, entry.options[CONF_ENTITY_ID]
44+
),
45+
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
46+
source_entity_removed=source_entity_removed,
47+
)
48+
)
49+
2450
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
2551
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
2652

tests/components/trend/test_init.py

Lines changed: 273 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,95 @@
11
"""Test the Trend integration."""
22

3+
from unittest.mock import patch
4+
5+
import pytest
6+
7+
from homeassistant.components import trend
8+
from homeassistant.components.trend.config_flow import ConfigFlowHandler
39
from homeassistant.components.trend.const import DOMAIN
4-
from homeassistant.config_entries import ConfigEntryState
5-
from homeassistant.core import HomeAssistant
10+
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
11+
from homeassistant.core import Event, HomeAssistant
612
from homeassistant.helpers import device_registry as dr, entity_registry as er
13+
from homeassistant.helpers.event import async_track_entity_registry_updated_event
714

815
from .conftest import ComponentSetup
916

1017
from tests.common import MockConfigEntry
1118

1219

20+
@pytest.fixture
21+
def sensor_config_entry(hass: HomeAssistant) -> er.RegistryEntry:
22+
"""Fixture to create a sensor config entry."""
23+
sensor_config_entry = MockConfigEntry()
24+
sensor_config_entry.add_to_hass(hass)
25+
return sensor_config_entry
26+
27+
28+
@pytest.fixture
29+
def sensor_device(
30+
device_registry: dr.DeviceRegistry, sensor_config_entry: ConfigEntry
31+
) -> dr.DeviceEntry:
32+
"""Fixture to create a sensor device."""
33+
return device_registry.async_get_or_create(
34+
config_entry_id=sensor_config_entry.entry_id,
35+
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
36+
)
37+
38+
39+
@pytest.fixture
40+
def sensor_entity_entry(
41+
entity_registry: er.EntityRegistry,
42+
sensor_config_entry: ConfigEntry,
43+
sensor_device: dr.DeviceEntry,
44+
) -> er.RegistryEntry:
45+
"""Fixture to create a sensor entity entry."""
46+
return entity_registry.async_get_or_create(
47+
"sensor",
48+
"test",
49+
"unique",
50+
config_entry=sensor_config_entry,
51+
device_id=sensor_device.id,
52+
original_name="ABC",
53+
)
54+
55+
56+
@pytest.fixture
57+
def trend_config_entry(
58+
hass: HomeAssistant,
59+
sensor_entity_entry: er.RegistryEntry,
60+
) -> MockConfigEntry:
61+
"""Fixture to create a trend config entry."""
62+
config_entry = MockConfigEntry(
63+
data={},
64+
domain=DOMAIN,
65+
options={
66+
"name": "My trend",
67+
"entity_id": sensor_entity_entry.entity_id,
68+
"invert": False,
69+
},
70+
title="My trend",
71+
version=ConfigFlowHandler.VERSION,
72+
minor_version=ConfigFlowHandler.MINOR_VERSION,
73+
)
74+
75+
config_entry.add_to_hass(hass)
76+
77+
return config_entry
78+
79+
80+
def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]:
81+
"""Track entity registry actions for an entity."""
82+
events = []
83+
84+
def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None:
85+
"""Add entity registry updated event to the list."""
86+
events.append(event.data["action"])
87+
88+
async_track_entity_registry_updated_event(hass, entity_id, add_event)
89+
90+
return events
91+
92+
1393
async def test_setup_and_remove_config_entry(
1494
hass: HomeAssistant,
1595
entity_registry: er.EntityRegistry,
@@ -135,3 +215,194 @@ async def test_device_cleaning(
135215
trend_config_entry.entry_id
136216
)
137217
assert len(devices_after_reload) == 1
218+
219+
220+
async def test_async_handle_source_entity_changes_source_entity_removed(
221+
hass: HomeAssistant,
222+
device_registry: dr.DeviceRegistry,
223+
entity_registry: er.EntityRegistry,
224+
trend_config_entry: MockConfigEntry,
225+
sensor_config_entry: ConfigEntry,
226+
sensor_device: dr.DeviceEntry,
227+
sensor_entity_entry: er.RegistryEntry,
228+
) -> None:
229+
"""Test the trend config entry is removed when the source entity is removed."""
230+
# Add another config entry to the sensor device
231+
other_config_entry = MockConfigEntry()
232+
other_config_entry.add_to_hass(hass)
233+
device_registry.async_update_device(
234+
sensor_device.id, add_config_entry_id=other_config_entry.entry_id
235+
)
236+
237+
assert await hass.config_entries.async_setup(trend_config_entry.entry_id)
238+
await hass.async_block_till_done()
239+
240+
trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend")
241+
assert trend_entity_entry.device_id == sensor_entity_entry.device_id
242+
243+
sensor_device = device_registry.async_get(sensor_device.id)
244+
assert trend_config_entry.entry_id in sensor_device.config_entries
245+
246+
events = track_entity_registry_actions(hass, trend_entity_entry.entity_id)
247+
248+
# Remove the source sensor's config entry from the device, this removes the
249+
# source sensor
250+
with patch(
251+
"homeassistant.components.trend.async_unload_entry",
252+
wraps=trend.async_unload_entry,
253+
) as mock_unload_entry:
254+
device_registry.async_update_device(
255+
sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id
256+
)
257+
await hass.async_block_till_done()
258+
await hass.async_block_till_done()
259+
mock_unload_entry.assert_called_once()
260+
261+
# Check that the trend config entry is removed from the device
262+
sensor_device = device_registry.async_get(sensor_device.id)
263+
assert trend_config_entry.entry_id not in sensor_device.config_entries
264+
265+
# Check that the trend config entry is removed
266+
assert trend_config_entry.entry_id not in hass.config_entries.async_entry_ids()
267+
268+
# Check we got the expected events
269+
assert events == ["remove"]
270+
271+
272+
async def test_async_handle_source_entity_changes_source_entity_removed_from_device(
273+
hass: HomeAssistant,
274+
device_registry: dr.DeviceRegistry,
275+
entity_registry: er.EntityRegistry,
276+
trend_config_entry: MockConfigEntry,
277+
sensor_device: dr.DeviceEntry,
278+
sensor_entity_entry: er.RegistryEntry,
279+
) -> None:
280+
"""Test the source entity removed from the source device."""
281+
assert await hass.config_entries.async_setup(trend_config_entry.entry_id)
282+
await hass.async_block_till_done()
283+
284+
trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend")
285+
assert trend_entity_entry.device_id == sensor_entity_entry.device_id
286+
287+
sensor_device = device_registry.async_get(sensor_device.id)
288+
assert trend_config_entry.entry_id in sensor_device.config_entries
289+
290+
events = track_entity_registry_actions(hass, trend_entity_entry.entity_id)
291+
292+
# Remove the source sensor from the device
293+
with patch(
294+
"homeassistant.components.trend.async_unload_entry",
295+
wraps=trend.async_unload_entry,
296+
) as mock_unload_entry:
297+
entity_registry.async_update_entity(
298+
sensor_entity_entry.entity_id, device_id=None
299+
)
300+
await hass.async_block_till_done()
301+
mock_unload_entry.assert_called_once()
302+
303+
# Check that the trend config entry is removed from the device
304+
sensor_device = device_registry.async_get(sensor_device.id)
305+
assert trend_config_entry.entry_id not in sensor_device.config_entries
306+
307+
# Check that the trend config entry is not removed
308+
assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids()
309+
310+
# Check we got the expected events
311+
assert events == ["update"]
312+
313+
314+
async def test_async_handle_source_entity_changes_source_entity_moved_other_device(
315+
hass: HomeAssistant,
316+
device_registry: dr.DeviceRegistry,
317+
entity_registry: er.EntityRegistry,
318+
trend_config_entry: MockConfigEntry,
319+
sensor_config_entry: ConfigEntry,
320+
sensor_device: dr.DeviceEntry,
321+
sensor_entity_entry: er.RegistryEntry,
322+
) -> None:
323+
"""Test the source entity is moved to another device."""
324+
sensor_device_2 = device_registry.async_get_or_create(
325+
config_entry_id=sensor_config_entry.entry_id,
326+
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")},
327+
)
328+
329+
assert await hass.config_entries.async_setup(trend_config_entry.entry_id)
330+
await hass.async_block_till_done()
331+
332+
trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend")
333+
assert trend_entity_entry.device_id == sensor_entity_entry.device_id
334+
335+
sensor_device = device_registry.async_get(sensor_device.id)
336+
assert trend_config_entry.entry_id in sensor_device.config_entries
337+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
338+
assert trend_config_entry.entry_id not in sensor_device_2.config_entries
339+
340+
events = track_entity_registry_actions(hass, trend_entity_entry.entity_id)
341+
342+
# Move the source sensor to another device
343+
with patch(
344+
"homeassistant.components.trend.async_unload_entry",
345+
wraps=trend.async_unload_entry,
346+
) as mock_unload_entry:
347+
entity_registry.async_update_entity(
348+
sensor_entity_entry.entity_id, device_id=sensor_device_2.id
349+
)
350+
await hass.async_block_till_done()
351+
mock_unload_entry.assert_called_once()
352+
353+
# Check that the trend config entry is moved to the other device
354+
sensor_device = device_registry.async_get(sensor_device.id)
355+
assert trend_config_entry.entry_id not in sensor_device.config_entries
356+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
357+
assert trend_config_entry.entry_id in sensor_device_2.config_entries
358+
359+
# Check that the trend config entry is not removed
360+
assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids()
361+
362+
# Check we got the expected events
363+
assert events == ["update"]
364+
365+
366+
async def test_async_handle_source_entity_new_entity_id(
367+
hass: HomeAssistant,
368+
device_registry: dr.DeviceRegistry,
369+
entity_registry: er.EntityRegistry,
370+
trend_config_entry: MockConfigEntry,
371+
sensor_device: dr.DeviceEntry,
372+
sensor_entity_entry: er.RegistryEntry,
373+
) -> None:
374+
"""Test the source entity's entity ID is changed."""
375+
assert await hass.config_entries.async_setup(trend_config_entry.entry_id)
376+
await hass.async_block_till_done()
377+
378+
trend_entity_entry = entity_registry.async_get("binary_sensor.my_trend")
379+
assert trend_entity_entry.device_id == sensor_entity_entry.device_id
380+
381+
sensor_device = device_registry.async_get(sensor_device.id)
382+
assert trend_config_entry.entry_id in sensor_device.config_entries
383+
384+
events = track_entity_registry_actions(hass, trend_entity_entry.entity_id)
385+
386+
# Change the source entity's entity ID
387+
with patch(
388+
"homeassistant.components.trend.async_unload_entry",
389+
wraps=trend.async_unload_entry,
390+
) as mock_unload_entry:
391+
entity_registry.async_update_entity(
392+
sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id"
393+
)
394+
await hass.async_block_till_done()
395+
mock_unload_entry.assert_called_once()
396+
397+
# Check that the trend config entry is updated with the new entity ID
398+
assert trend_config_entry.options["entity_id"] == "sensor.new_entity_id"
399+
400+
# Check that the helper config is still in the device
401+
sensor_device = device_registry.async_get(sensor_device.id)
402+
assert trend_config_entry.entry_id in sensor_device.config_entries
403+
404+
# Check that the trend config entry is not removed
405+
assert trend_config_entry.entry_id in hass.config_entries.async_entry_ids()
406+
407+
# Check we got the expected events
408+
assert events == []

0 commit comments

Comments
 (0)