Skip to content

Commit 9595759

Browse files
authored
Add stale device cleanup to Teslemetry (#144523)
1 parent d54f979 commit 9595759

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

homeassistant/components/teslemetry/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
9797
# Create the stream
9898
stream: TeslemetryStream | None = None
9999

100+
# Remember each device identifier we create
101+
current_devices: set[tuple[str, str]] = set()
102+
100103
for product in products:
101104
if (
102105
"vin" in product
@@ -116,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
116119
model=api.model,
117120
serial_number=vin,
118121
)
122+
current_devices.add((DOMAIN, vin))
119123

120124
# Create stream if required
121125
if not stream:
@@ -171,6 +175,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
171175
name=product.get("site_name", "Energy Site"),
172176
serial_number=str(site_id),
173177
)
178+
current_devices.add((DOMAIN, str(site_id)))
179+
180+
if wall_connector:
181+
for connector in product["components"]["wall_connectors"]:
182+
current_devices.add((DOMAIN, connector["din"]))
174183

175184
# Check live status endpoint works before creating its coordinator
176185
try:
@@ -235,6 +244,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
235244
config_entry_id=entry.entry_id, **energysite.device
236245
)
237246

247+
# Remove devices that are no longer present
248+
for device_entry in dr.async_entries_for_config_entry(
249+
device_registry, entry.entry_id
250+
):
251+
if not any(
252+
identifier in current_devices for identifier in device_entry.identifiers
253+
):
254+
LOGGER.debug("Removing stale device %s", device_entry.id)
255+
device_registry.async_update_device(
256+
device_id=device_entry.id,
257+
remove_config_entry_id=entry.entry_id,
258+
)
259+
238260
# Setup Platforms
239261
entry.runtime_data = TeslemetryData(vehicles, energysites, scopes, stream)
240262
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

tests/components/teslemetry/test_init.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Test the Teslemetry init."""
22

3-
from unittest.mock import AsyncMock
3+
from unittest.mock import AsyncMock, patch
44

55
from freezegun.api import FrozenDateTimeFactory
66
import pytest
@@ -11,6 +11,7 @@
1111
TeslaFleetError,
1212
)
1313

14+
from homeassistant.components.teslemetry.const import DOMAIN
1415
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
1516
from homeassistant.components.teslemetry.models import TeslemetryData
1617
from homeassistant.config_entries import ConfigEntryState
@@ -187,3 +188,94 @@ async def test_modern_no_poll(
187188
assert mock_vehicle_data.called is False
188189
freezer.tick(VEHICLE_INTERVAL)
189190
assert mock_vehicle_data.called is False
191+
192+
193+
async def test_stale_device_removal(
194+
hass: HomeAssistant,
195+
device_registry: dr.DeviceRegistry,
196+
mock_products: AsyncMock,
197+
) -> None:
198+
"""Test removal of stale devices."""
199+
200+
# Setup the entry first to get a valid config_entry_id
201+
entry = await setup_platform(hass)
202+
203+
# Create a device that should be removed (with the valid entry_id)
204+
device_registry.async_get_or_create(
205+
config_entry_id=entry.entry_id,
206+
identifiers={(DOMAIN, "stale-vin")},
207+
manufacturer="Tesla",
208+
name="Stale Vehicle",
209+
)
210+
211+
# Verify the stale device exists
212+
pre_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
213+
stale_identifiers = {
214+
identifier for device in pre_devices for identifier in device.identifiers
215+
}
216+
assert (DOMAIN, "stale-vin") in stale_identifiers
217+
218+
# Update products with an empty response (no devices) and reload entry
219+
with patch(
220+
"tesla_fleet_api.teslemetry.Teslemetry.products",
221+
return_value={"response": []},
222+
):
223+
await hass.config_entries.async_reload(entry.entry_id)
224+
await hass.async_block_till_done()
225+
226+
# Get updated devices after reload
227+
post_devices = dr.async_entries_for_config_entry(
228+
device_registry, entry.entry_id
229+
)
230+
post_identifiers = {
231+
identifier for device in post_devices for identifier in device.identifiers
232+
}
233+
234+
# Verify the stale device has been removed
235+
assert (DOMAIN, "stale-vin") not in post_identifiers
236+
237+
# Verify the device itself has been completely removed from the registry
238+
# since it had no other config entries
239+
updated_device = device_registry.async_get_device(
240+
identifiers={(DOMAIN, "stale-vin")}
241+
)
242+
assert updated_device is None
243+
244+
245+
async def test_device_retention_during_reload(
246+
hass: HomeAssistant,
247+
device_registry: dr.DeviceRegistry,
248+
mock_products: AsyncMock,
249+
) -> None:
250+
"""Test that valid devices are retained during a config entry reload."""
251+
# Setup entry with normal devices
252+
entry = await setup_platform(hass)
253+
254+
# Get initial device count and identifiers
255+
pre_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
256+
pre_count = len(pre_devices)
257+
pre_identifiers = {
258+
identifier for device in pre_devices for identifier in device.identifiers
259+
}
260+
261+
# Make sure we have some devices
262+
assert pre_count > 0
263+
264+
# Save the original identifiers to compare after reload
265+
original_identifiers = pre_identifiers.copy()
266+
267+
# Reload the config entry with the same products data
268+
# The mock_products fixture will return the same data as during setup
269+
await hass.config_entries.async_reload(entry.entry_id)
270+
await hass.async_block_till_done()
271+
272+
# Verify device count and identifiers after reload match pre-reload
273+
post_devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
274+
post_count = len(post_devices)
275+
post_identifiers = {
276+
identifier for device in post_devices for identifier in device.identifiers
277+
}
278+
279+
# Since the products data didn't change, we should have the same devices
280+
assert post_count == pre_count
281+
assert post_identifiers == original_identifiers

0 commit comments

Comments
 (0)