Remove deprecated process sensor from System monitor (#123616)

This commit is contained in:
G Johansson 2024-08-12 09:08:40 +02:00 committed by GitHub
parent 5b6bfa9ac8
commit 6343a086e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 60 additions and 430 deletions

View file

@ -73,7 +73,11 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry.""" """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} new_options = {**entry.options}
if entry.minor_version == 1: if entry.minor_version == 1:
# Migration copies process sensors to binary sensors # 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 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( _LOGGER.debug(
"Migration to version %s.%s successful", entry.version, entry.minor_version "Migration to version %s.%s successful", entry.version, entry.minor_version
) )

View file

@ -95,7 +95,7 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
config_flow = CONFIG_FLOW config_flow = CONFIG_FLOW
options_flow = OPTIONS_FLOW options_flow = OPTIONS_FLOW
VERSION = 1 VERSION = 1
MINOR_VERSION = 2 MINOR_VERSION = 3
def async_config_entry_title(self, options: Mapping[str, Any]) -> str: def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title.""" """Return config entry title."""

View file

@ -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()

View file

@ -14,8 +14,6 @@ import sys
import time import time
from typing import Any, Literal from typing import Any, Literal
from psutil import NoSuchProcess
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass, SensorDeviceClass,
@ -25,8 +23,6 @@ from homeassistant.components.sensor import (
) )
from homeassistant.const import ( from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
STATE_OFF,
STATE_ON,
EntityCategory, EntityCategory,
UnitOfDataRate, UnitOfDataRate,
UnitOfInformation, UnitOfInformation,
@ -36,13 +32,12 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback 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.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify from homeassistant.util import slugify
from . import SystemMonitorConfigEntry from . import SystemMonitorConfigEntry
from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES from .const import DOMAIN, NET_IO_TYPES
from .coordinator import SystemMonitorCoordinator from .coordinator import SystemMonitorCoordinator
from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature 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" 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: def get_network(entity: SystemMonitorSensor) -> float | None:
"""Return network in and out.""" """Return network in and out."""
counters = entity.coordinator.data.io_counters counters = entity.coordinator.data.io_counters
@ -341,15 +318,6 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
value_fn=get_throughput, value_fn=get_throughput,
add_to_update=lambda entity: ("io_counters", ""), 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( "processor_use": SysMonitorSensorEntityDescription(
key="processor_use", key="processor_use",
translation_key="processor_use", translation_key="processor_use",
@ -551,35 +519,6 @@ async def async_setup_entry(
) )
continue 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": if _type == "processor_use":
argument = "" argument = ""
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources) is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)

View file

@ -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": { "entity": {
"binary_sensor": { "binary_sensor": {
"process": { "process": {

View file

@ -35,7 +35,7 @@
}), }),
'disabled_by': None, 'disabled_by': None,
'domain': 'systemmonitor', 'domain': 'systemmonitor',
'minor_version': 2, 'minor_version': 3,
'options': dict({ 'options': dict({
'binary_sensor': dict({ 'binary_sensor': dict({
'process': list([ 'process': list([

View file

@ -300,24 +300,6 @@
# name: test_sensor[System Monitor Packets out eth1 - state] # name: test_sensor[System Monitor Packets out eth1 - state]
'150' '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] # name: test_sensor[System Monitor Processor temperature - attributes]
ReadOnlyDict({ ReadOnlyDict({
'device_class': 'temperature', 'device_class': 'temperature',

View file

@ -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.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() 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") process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3")
assert process_sensor is not None assert process_sensor is not None
assert process_sensor.state == STATE_ON 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

View file

@ -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()

View file

@ -14,12 +14,10 @@ from homeassistant.components.systemmonitor.const import DOMAIN
from homeassistant.components.systemmonitor.coordinator import VirtualMemory from homeassistant.components.systemmonitor.coordinator import VirtualMemory
from homeassistant.components.systemmonitor.sensor import get_cpu_icon from homeassistant.components.systemmonitor.sensor import get_cpu_icon
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import MockProcess
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -38,7 +36,6 @@ async def test_sensor(
data={}, data={},
options={ options={
"binary_sensor": {"process": ["python3", "pip"]}, "binary_sensor": {"process": ["python3", "pip"]},
"sensor": {"process": ["python3", "pip"]},
"resources": [ "resources": [
"disk_use_percent_/", "disk_use_percent_/",
"disk_use_percent_/home/notexist/", "disk_use_percent_/home/notexist/",
@ -62,10 +59,6 @@ async def test_sensor(
"friendly_name": "System Monitor Memory free", "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( for entity in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id entity_registry, mock_config_entry.entry_id
): ):
@ -154,7 +147,6 @@ async def test_sensor_updating(
data={}, data={},
options={ options={
"binary_sensor": {"process": ["python3", "pip"]}, "binary_sensor": {"process": ["python3", "pip"]},
"sensor": {"process": ["python3", "pip"]},
"resources": [ "resources": [
"disk_use_percent_/", "disk_use_percent_/",
"disk_use_percent_/home/notexist/", "disk_use_percent_/home/notexist/",
@ -172,10 +164,6 @@ async def test_sensor_updating(
assert memory_sensor is not None assert memory_sensor is not None
assert memory_sensor.state == "40.0" 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") mock_psutil.virtual_memory.side_effect = Exception("Failed to update")
freezer.tick(timedelta(minutes=1)) freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass) async_fire_time_changed(hass)
@ -202,53 +190,6 @@ async def test_sensor_updating(
assert memory_sensor.state == "25.0" 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") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_network_sensors( async def test_sensor_network_sensors(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,