From dc17780e5baab14f5cd5f6a6ad9ab54102042fc8 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Tue, 5 Dec 2023 18:52:22 +0100 Subject: [PATCH] Remove device from known_devices upon import in ping device tracker (#105009) Co-authored-by: Joost Lekkerkerker --- .../components/device_tracker/legacy.py | 13 +++ .../components/ping/device_tracker.py | 98 ++++++++++++++----- .../components/device_tracker/test_legacy.py | 44 +++++++++ tests/components/ping/test_device_tracker.py | 41 +++++++- 4 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 tests/components/device_tracker/test_legacy.py diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 264926a65bf970..a17972526cfa64 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1036,6 +1036,19 @@ def update_config(path: str, dev_id: str, device: Device) -> None: out.write(dump(device_config)) +def remove_device_from_config(hass: HomeAssistant, device_id: str) -> None: + """Remove device from YAML configuration file.""" + path = hass.config.path(YAML_DEVICES) + devices = load_yaml_config_file(path) + devices.pop(device_id) + dumped = dump(devices) + + with open(path, "r+", encoding="utf8") as out: + out.seek(0) + out.truncate() + out.write(dumped) + + def get_gravatar_for_email(email: str) -> str: """Return an 80px Gravatar for the given email address. diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index ceff1b2e124d72..417659aad5c84f 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -11,9 +12,20 @@ ScannerEntity, SourceType, ) +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, + remove_device_from_config, +) +from homeassistant.config import load_yaml_config_file from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_NAME -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.const import ( + CONF_HOST, + CONF_HOSTS, + CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, +) +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -42,34 +54,66 @@ async def async_setup_scanner( ) -> bool: """Legacy init: import via config flow.""" - for dev_name, dev_host in config[CONF_HOSTS].items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_IMPORTED_BY: "device_tracker", - CONF_NAME: dev_name, - CONF_HOST: dev_host, - CONF_PING_COUNT: config[CONF_PING_COUNT], + async def _run_import(_: Event) -> None: + """Delete devices from known_device.yaml and import them via config flow.""" + _LOGGER.debug( + "Home Assistant successfully started, importing ping device tracker config entries now" + ) + + devices: dict[str, dict[str, Any]] = {} + try: + devices = await hass.async_add_executor_job( + load_yaml_config_file, hass.config.path(YAML_DEVICES) + ) + except (FileNotFoundError, HomeAssistantError): + _LOGGER.debug( + "No valid known_devices.yaml found, " + "skip removal of devices from known_devices.yaml" + ) + + for dev_name, dev_host in config[CONF_HOSTS].items(): + if dev_name in devices: + await hass.async_add_executor_job( + remove_device_from_config, hass, dev_name + ) + _LOGGER.debug("Removed device %s from known_devices.yaml", dev_name) + + if not hass.states.async_available(f"device_tracker.{dev_name}"): + hass.states.async_remove(f"device_tracker.{dev_name}") + + # run import after everything has been cleaned up + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_IMPORTED_BY: "device_tracker", + CONF_NAME: dev_name, + CONF_HOST: dev_host, + CONF_PING_COUNT: config[CONF_PING_COUNT], + }, + ) + ) + + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.6.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Ping", }, ) - ) - async_create_issue( - hass, - HOMEASSISTANT_DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.6.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Ping", - }, - ) + # delay the import until after Home Assistant has started and everything has been initialized, + # as the legacy device tracker entities will be restored after the legacy device tracker platforms + # have been set up, so we can only remove the entities from the state machine then + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _run_import) return True diff --git a/tests/components/device_tracker/test_legacy.py b/tests/components/device_tracker/test_legacy.py new file mode 100644 index 00000000000000..d7a2f33c23b5c2 --- /dev/null +++ b/tests/components/device_tracker/test_legacy.py @@ -0,0 +1,44 @@ +"""Tests for the legacy device tracker component.""" +from unittest.mock import mock_open, patch + +from homeassistant.components.device_tracker import legacy +from homeassistant.core import HomeAssistant +from homeassistant.util.yaml import dump + +from tests.common import patch_yaml_files + + +def test_remove_device_from_config(hass: HomeAssistant): + """Test the removal of a device from a config.""" + yaml_devices = { + "test": { + "hide_if_away": True, + "mac": "00:11:22:33:44:55", + "name": "Test name", + "picture": "/local/test.png", + "track": True, + }, + "test2": { + "hide_if_away": True, + "mac": "00:ab:cd:33:44:55", + "name": "Test2", + "picture": "/local/test2.png", + "track": True, + }, + } + mopen = mock_open() + + files = {legacy.YAML_DEVICES: dump(yaml_devices)} + with patch_yaml_files(files, True), patch( + "homeassistant.components.device_tracker.legacy.open", mopen + ): + legacy.remove_device_from_config(hass, "test") + + mopen().write.assert_called_once_with( + "test2:\n" + " hide_if_away: true\n" + " mac: 00:ab:cd:33:44:55\n" + " name: Test2\n" + " picture: /local/test2.png\n" + " track: true\n" + ) diff --git a/tests/components/ping/test_device_tracker.py b/tests/components/ping/test_device_tracker.py index b6cc6b42912474..5f5bb2132c1356 100644 --- a/tests/components/ping/test_device_tracker.py +++ b/tests/components/ping/test_device_tracker.py @@ -1,13 +1,17 @@ """Test the binary sensor platform of ping.""" +from unittest.mock import patch import pytest +from homeassistant.components.device_tracker import legacy from homeassistant.components.ping.const import DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.helpers import entity_registry as er, issue_registry as ir from homeassistant.setup import async_setup_component +from homeassistant.util.yaml import dump -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, patch_yaml_files @pytest.mark.usefixtures("setup_integration") @@ -56,7 +60,42 @@ async def test_import_issue_creation( ) await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + issue = issue_registry.async_get_issue( HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" ) assert issue + + +async def test_import_delete_known_devices( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +): + """Test if import deletes known devices.""" + yaml_devices = { + "test": { + "hide_if_away": True, + "mac": "00:11:22:33:44:55", + "name": "Test name", + "picture": "/local/test.png", + "track": True, + }, + } + files = {legacy.YAML_DEVICES: dump(yaml_devices)} + + with patch_yaml_files(files, True), patch( + "homeassistant.components.ping.device_tracker.remove_device_from_config" + ) as remove_device_from_config: + await async_setup_component( + hass, + "device_tracker", + {"device_tracker": {"platform": "ping", "hosts": {"test": "10.10.10.10"}}}, + ) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(remove_device_from_config.mock_calls) == 1