mirror of
https://github.com/home-assistant/core.git
synced 2026-09-28 02:18:10 -04:00
Remove unshared ScorpionTrack vehicles automatically (#181852)
This commit is contained in:
@@ -24,6 +24,7 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def async_add_new_vehicles() -> None:
|
||||
"""Add ignition sensors for vehicles newly included in the share."""
|
||||
known_vehicles.intersection_update(coordinator.vehicles_by_id)
|
||||
new_vehicles = coordinator.vehicles_by_id.keys() - known_vehicles
|
||||
if not new_vehicles:
|
||||
return
|
||||
|
||||
@@ -15,6 +15,7 @@ from pyscorpiontrack import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
@@ -28,6 +29,8 @@ type ScorpionTrackConfigEntry = ConfigEntry[ScorpionTrackCoordinator]
|
||||
class ScorpionTrackCoordinator(DataUpdateCoordinator[ScorpionTrackShare]):
|
||||
"""Coordinate shared-location updates."""
|
||||
|
||||
config_entry: ScorpionTrackConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -68,4 +71,14 @@ class ScorpionTrackCoordinator(DataUpdateCoordinator[ScorpionTrackShare]):
|
||||
) from err
|
||||
else:
|
||||
self.vehicles_by_id = {vehicle.id: vehicle for vehicle in share.vehicles}
|
||||
device_registry = dr.async_get(self.hass)
|
||||
vehicle_identifiers = {
|
||||
(DOMAIN, f"{share.id}_{vehicle_id}")
|
||||
for vehicle_id in self.vehicles_by_id
|
||||
}
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_registry, self.config_entry.entry_id
|
||||
):
|
||||
if device.identifiers.isdisjoint(vehicle_identifiers):
|
||||
device_registry.async_remove_device(device.id)
|
||||
return share
|
||||
|
||||
@@ -26,6 +26,7 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def async_add_new_vehicles() -> None:
|
||||
"""Add trackers for vehicles newly included in the share."""
|
||||
known_vehicles.intersection_update(coordinator.vehicles_by_id)
|
||||
new_vehicles = coordinator.vehicles_by_id.keys() - known_vehicles
|
||||
if not new_vehicles:
|
||||
return
|
||||
|
||||
@@ -83,7 +83,7 @@ rules:
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -79,6 +79,7 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def async_add_new_vehicles() -> None:
|
||||
"""Add sensors for vehicles newly included in the share."""
|
||||
known_vehicles.intersection_update(coordinator.vehicles_by_id)
|
||||
new_vehicles = coordinator.vehicles_by_id.keys() - known_vehicles
|
||||
if not new_vehicles:
|
||||
return
|
||||
|
||||
@@ -93,14 +93,14 @@ async def test_ignition_binary_sensor_availability(
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_removed_vehicle_makes_ignition_binary_sensor_unavailable(
|
||||
async def test_removed_vehicle_removes_ignition_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test ignition becomes unavailable if its vehicle leaves the share."""
|
||||
"""Test ignition is removed if its vehicle leaves the share."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
@@ -110,9 +110,7 @@ async def test_removed_vehicle_makes_ignition_binary_sensor_unavailable(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert hass.states.get(ENTITY_ID) is None
|
||||
|
||||
|
||||
async def test_ignition_binary_sensor_uses_existing_vehicle_device(
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""Test automatic removal of ScorpionTrack vehicles."""
|
||||
|
||||
from dataclasses import replace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyscorpiontrack import (
|
||||
ScorpionTrackConnectionError,
|
||||
ScorpionTrackInvalidTokenError,
|
||||
ScorpionTrackShare,
|
||||
ScorpionTrackShareUnavailableError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.scorpiontrack.const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("latitude", [51.5074, None])
|
||||
async def test_shared_vehicle_is_kept(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
latitude: float | None,
|
||||
) -> None:
|
||||
"""Keep shared vehicles even when their GPS location is missing."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "101_1"), mock_config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
vehicle = mock_share.vehicles[0]
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share,
|
||||
vehicles=(
|
||||
replace(vehicle, position=replace(vehicle.position, latitude=latitude)),
|
||||
),
|
||||
)
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(device.id) == device
|
||||
assert mock_scorpiontrack_client.async_get_share.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
ScorpionTrackConnectionError(),
|
||||
ScorpionTrackInvalidTokenError(),
|
||||
ScorpionTrackShareUnavailableError(),
|
||||
],
|
||||
)
|
||||
async def test_failed_update_keeps_devices(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Remove absent vehicles only once a successful update confirms it."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "101_1"), mock_config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
mock_scorpiontrack_client.async_get_share.side_effect = exception
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(device.id) == device
|
||||
assert (
|
||||
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
|
||||
== entities
|
||||
)
|
||||
assert hass.states.get("sensor.ab12_cde_speed").state == STATE_UNAVAILABLE
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.side_effect = None
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share, vehicles=()
|
||||
)
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(device.id) is None
|
||||
assert not er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def test_removed_vehicle_returns(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Remove only this share's device and rediscover it without a reload."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
entity_registry.async_update_entity(
|
||||
"sensor.ab12_cde_speed", new_entity_id="sensor.vehicle_speed", name="My speed"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
original_entities = {
|
||||
entity.entity_id: entity.id
|
||||
for entity in er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
}
|
||||
other_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="202", data={"share_token": "other-token"}
|
||||
)
|
||||
other_client = AsyncMock()
|
||||
other_client.async_get_share.return_value = replace(mock_share, id=202)
|
||||
with patch(
|
||||
"homeassistant.components.scorpiontrack.ScorpionTrackClient",
|
||||
return_value=other_client,
|
||||
):
|
||||
await setup_integration(hass, other_entry)
|
||||
other_entities = er.async_entries_for_config_entry(
|
||||
entity_registry, other_entry.entry_id
|
||||
)
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "101_1"), mock_config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share, vehicles=()
|
||||
)
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(device.id) is None
|
||||
assert not er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert all(hass.states.get(entity_id) is None for entity_id in original_entities)
|
||||
assert er.async_entries_for_config_entry(entity_registry, other_entry.entry_id) == (
|
||||
other_entities
|
||||
)
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = mock_share
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert {
|
||||
entity.entity_id: entity.id
|
||||
for entity in er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
} == original_entities
|
||||
assert hass.states.get("sensor.vehicle_speed").state != STATE_UNAVAILABLE
|
||||
assert entity_registry.async_get("sensor.vehicle_speed").name == "My speed"
|
||||
assert (
|
||||
device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "101_1"), mock_config_entry.entry_id
|
||||
)
|
||||
is not None
|
||||
)
|
||||
assert (
|
||||
entity_registry.async_get("sensor.ab12_cde_heading").disabled_by
|
||||
is er.RegistryEntryDisabler.INTEGRATION
|
||||
)
|
||||
assert er.async_entries_for_config_entry(entity_registry, other_entry.entry_id) == (
|
||||
other_entities
|
||||
)
|
||||
assert mock_scorpiontrack_client.async_get_share.await_count == 3
|
||||
|
||||
|
||||
async def test_vehicle_removed_while_unloaded(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Clean up saved devices absent from the first successful setup response."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, "101_1"), mock_config_entry.entry_id
|
||||
)
|
||||
assert device is not None
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share, vehicles=()
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(device.id) is None
|
||||
assert not er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
@@ -33,14 +33,14 @@ async def test_device_tracker_state(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_removed_vehicle_becomes_unavailable(
|
||||
async def test_removed_vehicle_removes_tracker(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a tracker becomes unavailable if its vehicle leaves the share."""
|
||||
"""Test a tracker is removed if its vehicle leaves the share."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
@@ -50,9 +50,7 @@ async def test_removed_vehicle_becomes_unavailable(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("device_tracker.ab12_cde")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert hass.states.get("device_tracker.ab12_cde") is None
|
||||
|
||||
|
||||
async def test_connection_error_makes_tracker_unavailable(
|
||||
|
||||
@@ -147,7 +147,9 @@ async def test_vehicle_added_after_setup(
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
assert hass.states.get(entity_id) is None
|
||||
assert entity_registry.async_get(entity_id) is None
|
||||
assert device_registry.async_get(device.id) is None
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = updated_share
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
@@ -194,7 +194,7 @@ async def test_speed_sensor_availability(
|
||||
pytest.param(HEADING_ENTITY_ID, id="heading"),
|
||||
],
|
||||
)
|
||||
async def test_removed_vehicle_makes_sensor_unavailable(
|
||||
async def test_removed_vehicle_removes_sensor(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
@@ -202,7 +202,7 @@ async def test_removed_vehicle_makes_sensor_unavailable(
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test a sensor becomes unavailable if its vehicle leaves the share."""
|
||||
"""Test a sensor is removed if its vehicle leaves the share."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
@@ -212,9 +212,7 @@ async def test_removed_vehicle_makes_sensor_unavailable(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
Reference in New Issue
Block a user