diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 3fbc9edec2a..4a794a00432 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -73,7 +73,11 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - if entry.version == 1: + if entry.version > 1: + # This means the user has downgraded from a future version + return False + + if entry.version == 1 and entry.minor_version < 3: new_options = {**entry.options} if entry.minor_version == 1: # Migration copies process sensors to binary sensors @@ -84,6 +88,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, options=new_options, version=1, minor_version=2 ) + if entry.minor_version == 2: + new_options = {**entry.options} + if SENSOR_DOMAIN in new_options: + new_options.pop(SENSOR_DOMAIN) + hass.config_entries.async_update_entry( + entry, options=new_options, version=1, minor_version=3 + ) + _LOGGER.debug( "Migration to version %s.%s successful", entry.version, entry.minor_version ) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py index 0ff882d89da..34b28a1d47a 100644 --- a/homeassistant/components/systemmonitor/config_flow.py +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -95,7 +95,7 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW VERSION = 1 - MINOR_VERSION = 2 + MINOR_VERSION = 3 def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/systemmonitor/repairs.py b/homeassistant/components/systemmonitor/repairs.py deleted file mode 100644 index 10b5d18830d..00000000000 --- a/homeassistant/components/systemmonitor/repairs.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Repairs platform for the System Monitor integration.""" - -from __future__ import annotations - -from typing import Any, cast - -from homeassistant import data_entry_flow -from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - - -class ProcessFixFlow(RepairsFlow): - """Handler for an issue fixing flow.""" - - def __init__(self, entry: ConfigEntry, processes: list[str]) -> None: - """Create flow.""" - super().__init__() - self.entry = entry - self._processes = processes - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the first step of a fix flow.""" - return await self.async_step_migrate_process_sensor() - - async def async_step_migrate_process_sensor( - self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the options step of a fix flow.""" - if user_input is None: - return self.async_show_form( - step_id="migrate_process_sensor", - description_placeholders={"processes": ", ".join(self._processes)}, - ) - - # Migration has copied the sensors to binary sensors - # Pop the sensors to repair and remove entities - new_options: dict[str, Any] = self.entry.options.copy() - new_options.pop(SENSOR_DOMAIN) - - entity_reg = er.async_get(self.hass) - entries = er.async_entries_for_config_entry(entity_reg, self.entry.entry_id) - for entry in entries: - if entry.entity_id.startswith("sensor.") and entry.unique_id.startswith( - "process_" - ): - entity_reg.async_remove(entry.entity_id) - - self.hass.config_entries.async_update_entry(self.entry, options=new_options) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_create_entry(data={}) - - -async def async_create_fix_flow( - hass: HomeAssistant, - issue_id: str, - data: dict[str, Any] | None, -) -> RepairsFlow: - """Create flow.""" - entry = None - if data and (entry_id := data.get("entry_id")): - entry_id = cast(str, entry_id) - processes: list[str] = data["processes"] - entry = hass.config_entries.async_get_entry(entry_id) - assert entry - return ProcessFixFlow(entry, processes) - - return ConfirmRepairFlow() diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index bad4c3be0b5..ef1153f09e8 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -14,8 +14,6 @@ import sys import time from typing import Any, Literal -from psutil import NoSuchProcess - from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -25,8 +23,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( PERCENTAGE, - STATE_OFF, - STATE_ON, EntityCategory, UnitOfDataRate, UnitOfInformation, @@ -36,13 +32,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify from . import SystemMonitorConfigEntry -from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES +from .const import DOMAIN, NET_IO_TYPES from .coordinator import SystemMonitorCoordinator from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature @@ -68,24 +63,6 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: return "mdi:cpu-32-bit" -def get_process(entity: SystemMonitorSensor) -> str: - """Return process.""" - state = STATE_OFF - for proc in entity.coordinator.data.processes: - try: - _LOGGER.debug("process %s for argument %s", proc.name(), entity.argument) - if entity.argument == proc.name(): - state = STATE_ON - break - except NoSuchProcess as err: - _LOGGER.warning( - "Failed to load process with ID: %s, old name: %s", - err.pid, - err.name, - ) - return state - - def get_network(entity: SystemMonitorSensor) -> float | None: """Return network in and out.""" counters = entity.coordinator.data.io_counters @@ -341,15 +318,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { value_fn=get_throughput, add_to_update=lambda entity: ("io_counters", ""), ), - "process": SysMonitorSensorEntityDescription( - key="process", - translation_key="process", - placeholder="process", - icon=get_cpu_icon(), - mandatory_arg=True, - value_fn=get_process, - add_to_update=lambda entity: ("processes", ""), - ), "processor_use": SysMonitorSensorEntityDescription( key="processor_use", translation_key="processor_use", @@ -551,35 +519,6 @@ async def async_setup_entry( ) continue - if _type == "process": - _entry = entry.options.get(SENSOR_DOMAIN, {}) - for argument in _entry.get(CONF_PROCESS, []): - loaded_resources.add(slugify(f"{_type}_{argument}")) - entities.append( - SystemMonitorSensor( - coordinator, - sensor_description, - entry.entry_id, - argument, - True, - ) - ) - async_create_issue( - hass, - DOMAIN, - "process_sensor", - breaks_in_ha_version="2024.9.0", - is_fixable=True, - is_persistent=False, - severity=IssueSeverity.WARNING, - translation_key="process_sensor", - data={ - "entry_id": entry.entry_id, - "processes": _entry[CONF_PROCESS], - }, - ) - continue - if _type == "processor_use": argument = "" is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources) diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index aae2463c9da..dde97918bc3 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -22,19 +22,6 @@ } } }, - "issues": { - "process_sensor": { - "title": "Process sensors are deprecated and will be removed", - "fix_flow": { - "step": { - "migrate_process_sensor": { - "title": "Process sensors have been setup as binary sensors", - "description": "Process sensors `{processes}` have been created as binary sensors and the sensors will be removed in 2024.9.0.\n\nPlease update all automations, scripts, dashboards or other things depending on these sensors to use the newly created binary sensors instead and press **Submit** to fix this issue." - } - } - } - } - }, "entity": { "binary_sensor": { "process": { diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index b50e051c816..328065f6098 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -35,7 +35,7 @@ }), 'disabled_by': None, 'domain': 'systemmonitor', - 'minor_version': 2, + 'minor_version': 3, 'options': dict({ 'binary_sensor': dict({ 'process': list([ diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 3fe9ae7e809..1ee9067a528 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -300,24 +300,6 @@ # name: test_sensor[System Monitor Packets out eth1 - state] '150' # --- -# name: test_sensor[System Monitor Process pip - attributes] - ReadOnlyDict({ - 'friendly_name': 'System Monitor Process pip', - 'icon': 'mdi:cpu-64-bit', - }) -# --- -# name: test_sensor[System Monitor Process pip - state] - 'on' -# --- -# name: test_sensor[System Monitor Process python3 - attributes] - ReadOnlyDict({ - 'friendly_name': 'System Monitor Process python3', - 'icon': 'mdi:cpu-64-bit', - }) -# --- -# name: test_sensor[System Monitor Process python3 - state] - 'on' -# --- # name: test_sensor[System Monitor Processor temperature - attributes] ReadOnlyDict({ 'device_class': 'temperature', diff --git a/tests/components/systemmonitor/test_init.py b/tests/components/systemmonitor/test_init.py index 97f4a41b96c..6c1e4e6316c 100644 --- a/tests/components/systemmonitor/test_init.py +++ b/tests/components/systemmonitor/test_init.py @@ -95,9 +95,49 @@ async def test_migrate_process_sensor_to_binary_sensors( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None assert process_sensor.state == STATE_ON + + assert mock_config_entry.minor_version == 3 + assert mock_config_entry.options == { + "binary_sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + } + + +async def test_migration_from_future_version( + hass: HomeAssistant, + mock_psutil: Mock, + mock_os: Mock, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test migration from future version.""" + mock_config_entry = MockConfigEntry( + title="System Monitor", + domain=DOMAIN, + version=2, + data={}, + options={ + "sensor": {"process": ["python3", "pip"]}, + "resources": [ + "disk_use_percent_/", + "disk_use_percent_/home/notexist/", + "memory_free_", + "network_out_eth0", + "process_python3", + ], + }, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.MIGRATION_ERROR diff --git a/tests/components/systemmonitor/test_repairs.py b/tests/components/systemmonitor/test_repairs.py deleted file mode 100644 index 6c1ff9dfd16..00000000000 --- a/tests/components/systemmonitor/test_repairs.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Test repairs for System Monitor.""" - -from __future__ import annotations - -from http import HTTPStatus -from unittest.mock import Mock - -import pytest -from syrupy.assertion import SnapshotAssertion - -from homeassistant.components.repairs.websocket_api import ( - RepairsFlowIndexView, - RepairsFlowResourceView, -) -from homeassistant.components.systemmonitor.const import DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType -from homeassistant.helpers import entity_registry as er, issue_registry as ir -from homeassistant.setup import async_setup_component - -from tests.common import ANY, MockConfigEntry -from tests.typing import ClientSessionGenerator, WebSocketGenerator - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_migrate_process_sensor( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_psutil: Mock, - mock_os: Mock, - hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, - snapshot: SnapshotAssertion, -) -> None: - """Test migrating process sensor to binary sensor.""" - mock_config_entry = MockConfigEntry( - title="System Monitor", - domain=DOMAIN, - data={}, - options={ - "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, - "resources": [ - "disk_use_percent_/", - "disk_use_percent_/home/notexist/", - "memory_free_", - "network_out_eth0", - "process_python3", - ], - }, - ) - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert hass.config_entries.async_entries(DOMAIN) == snapshot( - name="before_migration" - ) - - assert await async_setup_component(hass, "repairs", {}) - await hass.async_block_till_done() - - entity = "sensor.system_monitor_process_python3" - state = hass.states.get(entity) - assert state - - assert entity_registry.async_get(entity) - - ws_client = await hass_ws_client(hass) - client = await hass_client() - - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - assert len(msg["result"]["issues"]) > 0 - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "process_sensor": - issue = i - assert issue is not None - - url = RepairsFlowIndexView.url - resp = await client.post( - url, json={"handler": DOMAIN, "issue_id": "process_sensor"} - ) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data["step_id"] == "migrate_process_sensor" - - url = RepairsFlowResourceView.url.format(flow_id=flow_id) - resp = await client.post(url, json={}) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - # Cannot use identity `is` check here as the value is parsed from JSON - assert data["type"] == FlowResultType.CREATE_ENTRY.value - await hass.async_block_till_done() - - state = hass.states.get("binary_sensor.system_monitor_process_python3") - assert state - - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - issue = None - for i in msg["result"]["issues"]: - if i["issue_id"] == "migrate_process_sensor": - issue = i - assert not issue - - entity = "sensor.system_monitor_process_python3" - state = hass.states.get(entity) - assert not state - - assert not entity_registry.async_get(entity) - - assert hass.config_entries.async_entries(DOMAIN) == snapshot(name="after_migration") - - -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_other_fixable_issues( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - hass_ws_client: WebSocketGenerator, - mock_added_config_entry: ConfigEntry, -) -> None: - """Test fixing other issues.""" - assert await async_setup_component(hass, "repairs", {}) - await hass.async_block_till_done() - - ws_client = await hass_ws_client(hass) - client = await hass_client() - - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - - issue = { - "breaks_in_ha_version": "2022.9.0dev0", - "domain": DOMAIN, - "issue_id": "issue_1", - "is_fixable": True, - "learn_more_url": "", - "severity": "error", - "translation_key": "issue_1", - } - ir.async_create_issue( - hass, - issue["domain"], - issue["issue_id"], - breaks_in_ha_version=issue["breaks_in_ha_version"], - is_fixable=issue["is_fixable"], - is_persistent=False, - learn_more_url=None, - severity=issue["severity"], - translation_key=issue["translation_key"], - ) - - await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - results = msg["result"]["issues"] - assert { - "breaks_in_ha_version": "2022.9.0dev0", - "created": ANY, - "dismissed_version": None, - "domain": DOMAIN, - "is_fixable": True, - "issue_domain": None, - "issue_id": "issue_1", - "learn_more_url": None, - "severity": "error", - "translation_key": "issue_1", - "translation_placeholders": None, - "ignored": False, - } in results - - url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "issue_1"}) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data["step_id"] == "confirm" - - url = RepairsFlowResourceView.url.format(flow_id=flow_id) - resp = await client.post(url) - assert resp.status == HTTPStatus.OK - data = await resp.json() - - # Cannot use identity `is` check here as the value is parsed from JSON - assert data["type"] == FlowResultType.CREATE_ENTRY.value - await hass.async_block_till_done() diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index ce15083da67..6d22c5354a4 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -14,12 +14,10 @@ from homeassistant.components.systemmonitor.const import DOMAIN from homeassistant.components.systemmonitor.coordinator import VirtualMemory from homeassistant.components.systemmonitor.sensor import get_cpu_icon from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockProcess - from tests.common import MockConfigEntry, async_fire_time_changed @@ -38,7 +36,6 @@ async def test_sensor( data={}, options={ "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, "resources": [ "disk_use_percent_/", "disk_use_percent_/home/notexist/", @@ -62,10 +59,6 @@ async def test_sensor( "friendly_name": "System Monitor Memory free", } - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - for entity in er.async_entries_for_config_entry( entity_registry, mock_config_entry.entry_id ): @@ -154,7 +147,6 @@ async def test_sensor_updating( data={}, options={ "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, "resources": [ "disk_use_percent_/", "disk_use_percent_/home/notexist/", @@ -172,10 +164,6 @@ async def test_sensor_updating( assert memory_sensor is not None assert memory_sensor.state == "40.0" - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - mock_psutil.virtual_memory.side_effect = Exception("Failed to update") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) @@ -202,53 +190,6 @@ async def test_sensor_updating( assert memory_sensor.state == "25.0" -async def test_sensor_process_fails( - hass: HomeAssistant, - mock_psutil: Mock, - mock_os: Mock, - freezer: FrozenDateTimeFactory, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test process not exist failure.""" - mock_config_entry = MockConfigEntry( - title="System Monitor", - domain=DOMAIN, - data={}, - options={ - "binary_sensor": {"process": ["python3", "pip"]}, - "sensor": {"process": ["python3", "pip"]}, - "resources": [ - "disk_use_percent_/", - "disk_use_percent_/home/notexist/", - "memory_free_", - "network_out_eth0", - "process_python3", - ], - }, - ) - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_ON - - _process = MockProcess("python3", True) - - mock_psutil.process_iter.return_value = [_process] - - freezer.tick(timedelta(minutes=1)) - async_fire_time_changed(hass) - await hass.async_block_till_done(wait_background_tasks=True) - - process_sensor = hass.states.get("sensor.system_monitor_process_python3") - assert process_sensor is not None - assert process_sensor.state == STATE_OFF - - assert "Failed to load process with ID: 1, old name: python3" in caplog.text - - @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor_network_sensors( freezer: FrozenDateTimeFactory,