Skip to content

Commit c02707a

Browse files
emontnemeryfrenck
authored andcommitted
Handle changes to source entity in statistics helper (home-assistant#146523)
1 parent 232f853 commit c02707a

File tree

2 files changed

+305
-4
lines changed

2 files changed

+305
-4
lines changed

homeassistant/components/statistics/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from homeassistant.const import CONF_ENTITY_ID, Platform
55
from homeassistant.core import HomeAssistant
66
from homeassistant.helpers.device import (
7+
async_entity_id_to_device_id,
78
async_remove_stale_devices_links_keep_entity_device,
89
)
10+
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
911

1012
DOMAIN = "statistics"
1113
PLATFORMS = [Platform.SENSOR]
@@ -20,6 +22,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
2022
entry.options[CONF_ENTITY_ID],
2123
)
2224

25+
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
26+
hass.config_entries.async_update_entry(
27+
entry,
28+
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
29+
)
30+
31+
async def source_entity_removed() -> None:
32+
# The source entity has been removed, we remove the config entry because
33+
# statistics does not allow replacing the input entity.
34+
await hass.config_entries.async_remove(entry.entry_id)
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_ENTITY_ID]
43+
),
44+
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
45+
source_entity_removed=source_entity_removed,
46+
)
47+
)
48+
2349
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
2450
entry.async_on_unload(entry.add_update_listener(update_listener))
2551

tests/components/statistics/test_init.py

Lines changed: 279 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,98 @@
22

33
from __future__ import annotations
44

5-
from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN
6-
from homeassistant.config_entries import ConfigEntryState
7-
from homeassistant.core import HomeAssistant
5+
from unittest.mock import patch
6+
7+
import pytest
8+
9+
from homeassistant.components import statistics
10+
from homeassistant.components.statistics import DOMAIN
11+
from homeassistant.components.statistics.config_flow import StatisticsConfigFlowHandler
12+
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
13+
from homeassistant.core import Event, HomeAssistant
814
from homeassistant.helpers import device_registry as dr, entity_registry as er
15+
from homeassistant.helpers.event import async_track_entity_registry_updated_event
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 statistics_config_entry(
58+
hass: HomeAssistant,
59+
sensor_entity_entry: er.RegistryEntry,
60+
) -> MockConfigEntry:
61+
"""Fixture to create a statistics config entry."""
62+
config_entry = MockConfigEntry(
63+
data={},
64+
domain=DOMAIN,
65+
options={
66+
"name": "My statistics",
67+
"entity_id": sensor_entity_entry.entity_id,
68+
"state_characteristic": "mean",
69+
"keep_last_sample": False,
70+
"percentile": 50.0,
71+
"precision": 2.0,
72+
"sampling_size": 20.0,
73+
},
74+
title="My statistics",
75+
version=StatisticsConfigFlowHandler.VERSION,
76+
minor_version=StatisticsConfigFlowHandler.MINOR_VERSION,
77+
)
78+
79+
config_entry.add_to_hass(hass)
80+
81+
return config_entry
82+
83+
84+
def track_entity_registry_actions(hass: HomeAssistant, entity_id: str) -> list[str]:
85+
"""Track entity registry actions for an entity."""
86+
events = []
87+
88+
def add_event(event: Event[er.EventEntityRegistryUpdatedData]) -> None:
89+
"""Add entity registry updated event to the list."""
90+
events.append(event.data["action"])
91+
92+
async_track_entity_registry_updated_event(hass, entity_id, add_event)
93+
94+
return events
95+
96+
1397
async def test_unload_entry(hass: HomeAssistant, loaded_entry: MockConfigEntry) -> None:
1498
"""Test unload an entry."""
1599

@@ -51,7 +135,7 @@ async def test_device_cleaning(
51135
# Configure the configuration entry for Statistics
52136
statistics_config_entry = MockConfigEntry(
53137
data={},
54-
domain=STATISTICS_DOMAIN,
138+
domain=DOMAIN,
55139
options={
56140
"name": "Statistics",
57141
"entity_id": "sensor.test_source",
@@ -107,3 +191,194 @@ async def test_device_cleaning(
107191
assert len(devices_after_reload) == 1
108192

109193
assert devices_after_reload[0].id == source_device1_entry.id
194+
195+
196+
async def test_async_handle_source_entity_changes_source_entity_removed(
197+
hass: HomeAssistant,
198+
device_registry: dr.DeviceRegistry,
199+
entity_registry: er.EntityRegistry,
200+
statistics_config_entry: MockConfigEntry,
201+
sensor_config_entry: ConfigEntry,
202+
sensor_device: dr.DeviceEntry,
203+
sensor_entity_entry: er.RegistryEntry,
204+
) -> None:
205+
"""Test the statistics config entry is removed when the source entity is removed."""
206+
# Add another config entry to the sensor device
207+
other_config_entry = MockConfigEntry()
208+
other_config_entry.add_to_hass(hass)
209+
device_registry.async_update_device(
210+
sensor_device.id, add_config_entry_id=other_config_entry.entry_id
211+
)
212+
213+
assert await hass.config_entries.async_setup(statistics_config_entry.entry_id)
214+
await hass.async_block_till_done()
215+
216+
statistics_entity_entry = entity_registry.async_get("sensor.my_statistics")
217+
assert statistics_entity_entry.device_id == sensor_entity_entry.device_id
218+
219+
sensor_device = device_registry.async_get(sensor_device.id)
220+
assert statistics_config_entry.entry_id in sensor_device.config_entries
221+
222+
events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id)
223+
224+
# Remove the source sensor's config entry from the device, this removes the
225+
# source sensor
226+
with patch(
227+
"homeassistant.components.statistics.async_unload_entry",
228+
wraps=statistics.async_unload_entry,
229+
) as mock_unload_entry:
230+
device_registry.async_update_device(
231+
sensor_device.id, remove_config_entry_id=sensor_config_entry.entry_id
232+
)
233+
await hass.async_block_till_done()
234+
await hass.async_block_till_done()
235+
mock_unload_entry.assert_called_once()
236+
237+
# Check that the statistics config entry is removed from the device
238+
sensor_device = device_registry.async_get(sensor_device.id)
239+
assert statistics_config_entry.entry_id not in sensor_device.config_entries
240+
241+
# Check that the statistics config entry is removed
242+
assert statistics_config_entry.entry_id not in hass.config_entries.async_entry_ids()
243+
244+
# Check we got the expected events
245+
assert events == ["remove"]
246+
247+
248+
async def test_async_handle_source_entity_changes_source_entity_removed_from_device(
249+
hass: HomeAssistant,
250+
device_registry: dr.DeviceRegistry,
251+
entity_registry: er.EntityRegistry,
252+
statistics_config_entry: MockConfigEntry,
253+
sensor_device: dr.DeviceEntry,
254+
sensor_entity_entry: er.RegistryEntry,
255+
) -> None:
256+
"""Test the source entity removed from the source device."""
257+
assert await hass.config_entries.async_setup(statistics_config_entry.entry_id)
258+
await hass.async_block_till_done()
259+
260+
statistics_entity_entry = entity_registry.async_get("sensor.my_statistics")
261+
assert statistics_entity_entry.device_id == sensor_entity_entry.device_id
262+
263+
sensor_device = device_registry.async_get(sensor_device.id)
264+
assert statistics_config_entry.entry_id in sensor_device.config_entries
265+
266+
events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id)
267+
268+
# Remove the source sensor from the device
269+
with patch(
270+
"homeassistant.components.statistics.async_unload_entry",
271+
wraps=statistics.async_unload_entry,
272+
) as mock_unload_entry:
273+
entity_registry.async_update_entity(
274+
sensor_entity_entry.entity_id, device_id=None
275+
)
276+
await hass.async_block_till_done()
277+
mock_unload_entry.assert_called_once()
278+
279+
# Check that the statistics config entry is removed from the device
280+
sensor_device = device_registry.async_get(sensor_device.id)
281+
assert statistics_config_entry.entry_id not in sensor_device.config_entries
282+
283+
# Check that the statistics config entry is not removed
284+
assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids()
285+
286+
# Check we got the expected events
287+
assert events == ["update"]
288+
289+
290+
async def test_async_handle_source_entity_changes_source_entity_moved_other_device(
291+
hass: HomeAssistant,
292+
device_registry: dr.DeviceRegistry,
293+
entity_registry: er.EntityRegistry,
294+
statistics_config_entry: MockConfigEntry,
295+
sensor_config_entry: ConfigEntry,
296+
sensor_device: dr.DeviceEntry,
297+
sensor_entity_entry: er.RegistryEntry,
298+
) -> None:
299+
"""Test the source entity is moved to another device."""
300+
sensor_device_2 = device_registry.async_get_or_create(
301+
config_entry_id=sensor_config_entry.entry_id,
302+
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:FF")},
303+
)
304+
305+
assert await hass.config_entries.async_setup(statistics_config_entry.entry_id)
306+
await hass.async_block_till_done()
307+
308+
statistics_entity_entry = entity_registry.async_get("sensor.my_statistics")
309+
assert statistics_entity_entry.device_id == sensor_entity_entry.device_id
310+
311+
sensor_device = device_registry.async_get(sensor_device.id)
312+
assert statistics_config_entry.entry_id in sensor_device.config_entries
313+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
314+
assert statistics_config_entry.entry_id not in sensor_device_2.config_entries
315+
316+
events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id)
317+
318+
# Move the source sensor to another device
319+
with patch(
320+
"homeassistant.components.statistics.async_unload_entry",
321+
wraps=statistics.async_unload_entry,
322+
) as mock_unload_entry:
323+
entity_registry.async_update_entity(
324+
sensor_entity_entry.entity_id, device_id=sensor_device_2.id
325+
)
326+
await hass.async_block_till_done()
327+
mock_unload_entry.assert_called_once()
328+
329+
# Check that the statistics config entry is moved to the other device
330+
sensor_device = device_registry.async_get(sensor_device.id)
331+
assert statistics_config_entry.entry_id not in sensor_device.config_entries
332+
sensor_device_2 = device_registry.async_get(sensor_device_2.id)
333+
assert statistics_config_entry.entry_id in sensor_device_2.config_entries
334+
335+
# Check that the statistics config entry is not removed
336+
assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids()
337+
338+
# Check we got the expected events
339+
assert events == ["update"]
340+
341+
342+
async def test_async_handle_source_entity_new_entity_id(
343+
hass: HomeAssistant,
344+
device_registry: dr.DeviceRegistry,
345+
entity_registry: er.EntityRegistry,
346+
statistics_config_entry: MockConfigEntry,
347+
sensor_device: dr.DeviceEntry,
348+
sensor_entity_entry: er.RegistryEntry,
349+
) -> None:
350+
"""Test the source entity's entity ID is changed."""
351+
assert await hass.config_entries.async_setup(statistics_config_entry.entry_id)
352+
await hass.async_block_till_done()
353+
354+
statistics_entity_entry = entity_registry.async_get("sensor.my_statistics")
355+
assert statistics_entity_entry.device_id == sensor_entity_entry.device_id
356+
357+
sensor_device = device_registry.async_get(sensor_device.id)
358+
assert statistics_config_entry.entry_id in sensor_device.config_entries
359+
360+
events = track_entity_registry_actions(hass, statistics_entity_entry.entity_id)
361+
362+
# Change the source entity's entity ID
363+
with patch(
364+
"homeassistant.components.statistics.async_unload_entry",
365+
wraps=statistics.async_unload_entry,
366+
) as mock_unload_entry:
367+
entity_registry.async_update_entity(
368+
sensor_entity_entry.entity_id, new_entity_id="sensor.new_entity_id"
369+
)
370+
await hass.async_block_till_done()
371+
mock_unload_entry.assert_called_once()
372+
373+
# Check that the statistics config entry is updated with the new entity ID
374+
assert statistics_config_entry.options["entity_id"] == "sensor.new_entity_id"
375+
376+
# Check that the helper config is still in the device
377+
sensor_device = device_registry.async_get(sensor_device.id)
378+
assert statistics_config_entry.entry_id in sensor_device.config_entries
379+
380+
# Check that the statistics config entry is not removed
381+
assert statistics_config_entry.entry_id in hass.config_entries.async_entry_ids()
382+
383+
# Check we got the expected events
384+
assert events == []

0 commit comments

Comments
 (0)