From 4f0ee20ec5dbad7bfcbd8a4940f83edbc629d183 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 26 Dec 2023 18:29:32 +0100 Subject: [PATCH] Add config flow to System Monitor (#104906) * Initial commit for config flow to System Monitor * sensors * Fixes * Works * Add import * entity_registry_enabled_default = False * entity_category = diagnostic * Create issue * issue in config flow * Tests * test requirement * codeowner * Fix names * processes * Fix type * reviews * get info during startup once * Select process * Legacy import of resources * requirements * Allow custom * Fix tests * strings * strings * Always enable process sensors * Fix docstrings * skip remove sensors if no sensors * Modify sensors * Fix tests --- .coveragerc | 2 + CODEOWNERS | 2 + .../components/systemmonitor/__init__.py | 26 +- .../components/systemmonitor/config_flow.py | 143 ++++++++++ .../components/systemmonitor/const.py | 17 ++ .../components/systemmonitor/manifest.json | 3 +- .../components/systemmonitor/sensor.py | 157 ++++++++-- .../components/systemmonitor/strings.json | 25 ++ .../components/systemmonitor/util.py | 42 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/systemmonitor/__init__.py | 1 + tests/components/systemmonitor/conftest.py | 17 ++ .../systemmonitor/test_config_flow.py | 270 ++++++++++++++++++ 15 files changed, 687 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/systemmonitor/config_flow.py create mode 100644 homeassistant/components/systemmonitor/const.py create mode 100644 homeassistant/components/systemmonitor/strings.json create mode 100644 homeassistant/components/systemmonitor/util.py create mode 100644 tests/components/systemmonitor/__init__.py create mode 100644 tests/components/systemmonitor/conftest.py create mode 100644 tests/components/systemmonitor/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a67ee911681..590f69961e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1305,7 +1305,9 @@ omit = homeassistant/components/system_bridge/notify.py homeassistant/components/system_bridge/sensor.py homeassistant/components/system_bridge/update.py + homeassistant/components/systemmonitor/__init__.py homeassistant/components/systemmonitor/sensor.py + homeassistant/components/systemmonitor/util.py homeassistant/components/tado/__init__.py homeassistant/components/tado/binary_sensor.py homeassistant/components/tado/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index b3d5475379e..b50af486033 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1297,6 +1297,8 @@ build.json @home-assistant/supervisor /homeassistant/components/synology_srm/ @aerialls /homeassistant/components/system_bridge/ @timmo001 /tests/components/system_bridge/ @timmo001 +/homeassistant/components/systemmonitor/ @gjohansson-ST +/tests/components/systemmonitor/ @gjohansson-ST /homeassistant/components/tado/ @michaelarnauts @chiefdragon /tests/components/tado/ @michaelarnauts @chiefdragon /homeassistant/components/tag/ @balloob @dmulcahey diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 5ab8ac9f930..69dbb1f7952 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -1 +1,25 @@ -"""The systemmonitor integration.""" +"""The System Monitor integration.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up System Monitor from a config entry.""" + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload System Monitor config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/systemmonitor/config_flow.py b/homeassistant/components/systemmonitor/config_flow.py new file mode 100644 index 00000000000..3dc45480aee --- /dev/null +++ b/homeassistant/components/systemmonitor/config_flow.py @@ -0,0 +1,143 @@ +"""Adds config flow for System Monitor.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaCommonFlowHandler, + SchemaConfigFlowHandler, + SchemaFlowFormStep, +) +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) +from homeassistant.util import slugify + +from .const import CONF_PROCESS, DOMAIN +from .util import get_all_running_processes + + +async def validate_sensor_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate sensor input.""" + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + sensors: dict[str, list] = handler.options.setdefault(SENSOR_DOMAIN, {}) + processes = sensors.setdefault(CONF_PROCESS, []) + previous_processes = processes.copy() + processes.clear() + processes.extend(user_input[CONF_PROCESS]) + + entity_registry = er.async_get(handler.parent_handler.hass) + for process in previous_processes: + if process not in processes and ( + entity_id := entity_registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, slugify(f"process_{process}") + ) + ): + entity_registry.async_remove(entity_id) + + return {} + + +async def validate_import_sensor_setup( + handler: SchemaCommonFlowHandler, user_input: dict[str, Any] +) -> dict[str, Any]: + """Validate sensor input.""" + # Standard behavior is to merge the result with the options. + # In this case, we want to add a sub-item so we update the options directly. + sensors: dict[str, list] = handler.options.setdefault(SENSOR_DOMAIN, {}) + import_processes: list[str] = user_input["processes"] + processes = sensors.setdefault(CONF_PROCESS, []) + processes.extend(import_processes) + legacy_resources: list[str] = handler.options.setdefault("resources", []) + legacy_resources.extend(user_input["legacy_resources"]) + + async_create_issue( + handler.parent_handler.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "System Monitor", + }, + ) + return {} + + +async def get_sensor_setup_schema(handler: SchemaCommonFlowHandler) -> vol.Schema: + """Return process sensor setup schema.""" + hass = handler.parent_handler.hass + processes = await hass.async_add_executor_job(get_all_running_processes) + return vol.Schema( + { + vol.Required(CONF_PROCESS): SelectSelector( + SelectSelectorConfig( + options=processes, + multiple=True, + custom_value=True, + mode=SelectSelectorMode.DROPDOWN, + sort=True, + ) + ) + } + ) + + +async def get_suggested_value(handler: SchemaCommonFlowHandler) -> dict[str, Any]: + """Return suggested values for sensor setup.""" + sensors: dict[str, list] = handler.options.get(SENSOR_DOMAIN, {}) + processes: list[str] = sensors.get(CONF_PROCESS, []) + return {CONF_PROCESS: processes} + + +CONFIG_FLOW = { + "user": SchemaFlowFormStep(schema=vol.Schema({})), + "import": SchemaFlowFormStep( + schema=vol.Schema({}), + validate_user_input=validate_import_sensor_setup, + ), +} +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + get_sensor_setup_schema, + suggested_values=get_suggested_value, + validate_user_input=validate_sensor_setup, + ) +} + + +class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for System Monitor.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return "System Monitor" + + @callback + def async_create_entry(self, data: Mapping[str, Any], **kwargs: Any) -> FlowResult: + """Finish config flow and create a config entry.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + return super().async_create_entry(data, **kwargs) diff --git a/homeassistant/components/systemmonitor/const.py b/homeassistant/components/systemmonitor/const.py new file mode 100644 index 00000000000..c92647f9c8e --- /dev/null +++ b/homeassistant/components/systemmonitor/const.py @@ -0,0 +1,17 @@ +"""Constants for System Monitor.""" + +DOMAIN = "systemmonitor" + +CONF_INDEX = "index" +CONF_PROCESS = "process" + +NETWORK_TYPES = [ + "network_in", + "network_out", + "throughput_network_in", + "throughput_network_out", + "packets_in", + "packets_out", + "ipv4_address", + "ipv6_address", +] diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 3288f4299dc..213fa9cf6be 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -1,7 +1,8 @@ { "domain": "systemmonitor", "name": "System Monitor", - "codeowners": [], + "codeowners": ["@gjohansson-ST"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "iot_class": "local_push", "loggers": ["psutil"], diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index f34686ca3da..57838c45dc7 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -15,26 +15,29 @@ import psutil import voluptuous as vol from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_RESOURCES, - CONF_SCAN_INTERVAL, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, STATE_OFF, STATE_ON, + EntityCategory, UnitOfDataRate, UnitOfInformation, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -46,6 +49,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from .const import CONF_PROCESS, DOMAIN, NETWORK_TYPES +from .util import get_all_disk_mounts, get_all_network_interfaces + _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" @@ -261,6 +267,17 @@ def check_required_arg(value: Any) -> Any: return value +def check_legacy_resource(resource: str, resources: list[str]) -> bool: + """Return True if legacy resource was configured.""" + # This function to check legacy resources can be removed + # once we are removing the import from YAML + if resource in resources: + _LOGGER.debug("Checking %s in %s returns True", resource, ", ".join(resources)) + return True + _LOGGER.debug("Checking %s in %s returns False", resource, ", ".join(resources)) + return False + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_RESOURCES, default={CONF_TYPE: "disk_use"}): vol.All( @@ -334,39 +351,126 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the system monitor sensors.""" + processes = [ + resource[CONF_ARG] + for resource in config[CONF_RESOURCES] + if resource[CONF_TYPE] == "process" + ] + legacy_config: list[dict[str, str]] = config[CONF_RESOURCES] + resources = [] + for resource_conf in legacy_config: + if (_type := resource_conf[CONF_TYPE]).startswith("disk_"): + if (arg := resource_conf.get(CONF_ARG)) is None: + resources.append(f"{_type}_/") + continue + resources.append(f"{_type}_{arg}") + continue + resources.append(f"{_type}_{resource_conf.get(CONF_ARG, '')}") + _LOGGER.debug( + "Importing config with processes: %s, resources: %s", processes, resources + ) + + # With removal of the import also cleanup legacy_resources logic in setup_entry + # Also cleanup entry.options["resources"] which is only imported for legacy reasons + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={"processes": processes, "legacy_resources": resources}, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up System Montor sensors based on a config entry.""" entities = [] sensor_registry: dict[tuple[str, str], SensorData] = {} + legacy_resources: list[str] = entry.options.get("resources", []) + disk_arguments = await hass.async_add_executor_job(get_all_disk_mounts) + network_arguments = await hass.async_add_executor_job(get_all_network_interfaces) + cpu_temperature = await hass.async_add_executor_job(_read_cpu_temperature) - for resource in config[CONF_RESOURCES]: - type_ = resource[CONF_TYPE] - # Initialize the sensor argument if none was provided. - # For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified. - if CONF_ARG not in resource: - argument = "" - if resource[CONF_TYPE].startswith("disk_"): - argument = "/" - else: - argument = resource[CONF_ARG] + _LOGGER.debug("Setup from options %s", entry.options) + + for _type, sensor_description in SENSOR_TYPES.items(): + if _type.startswith("disk_"): + for argument in disk_arguments: + sensor_registry[(_type, argument)] = SensorData( + argument, None, None, None, None + ) + is_enabled = check_legacy_resource( + f"{_type}_{argument}", legacy_resources + ) + entities.append( + SystemMonitorSensor( + sensor_registry, + sensor_description, + entry.entry_id, + argument, + is_enabled, + ) + ) + continue + + if _type in NETWORK_TYPES: + for argument in network_arguments: + sensor_registry[(_type, argument)] = SensorData( + argument, None, None, None, None + ) + is_enabled = check_legacy_resource( + f"{_type}_{argument}", legacy_resources + ) + entities.append( + SystemMonitorSensor( + sensor_registry, + sensor_description, + entry.entry_id, + argument, + is_enabled, + ) + ) + continue # Verify if we can retrieve CPU / processor temperatures. # If not, do not create the entity and add a warning to the log - if ( - type_ == "processor_temperature" - and await hass.async_add_executor_job(_read_cpu_temperature) is None - ): + if _type == "processor_temperature" and cpu_temperature is None: _LOGGER.warning("Cannot read CPU / processor temperature information") continue - sensor_registry[(type_, argument)] = SensorData( - argument, None, None, None, None - ) + if _type == "process": + _entry: dict[str, list] = entry.options.get(SENSOR_DOMAIN, {}) + for argument in _entry.get(CONF_PROCESS, []): + sensor_registry[(_type, argument)] = SensorData( + argument, None, None, None, None + ) + entities.append( + SystemMonitorSensor( + sensor_registry, + sensor_description, + entry.entry_id, + argument, + True, + ) + ) + continue + + sensor_registry[(_type, "")] = SensorData("", None, None, None, None) + is_enabled = check_legacy_resource(f"{_type}_", legacy_resources) entities.append( - SystemMonitorSensor(sensor_registry, SENSOR_TYPES[type_], argument) + SystemMonitorSensor( + sensor_registry, + sensor_description, + entry.entry_id, + "", + is_enabled, + ) ) - scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + scan_interval = DEFAULT_SCAN_INTERVAL await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval) - async_add_entities(entities) @@ -433,12 +537,16 @@ class SystemMonitorSensor(SensorEntity): """Implementation of a system monitor sensor.""" should_poll = False + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, sensor_registry: dict[tuple[str, str], SensorData], sensor_description: SysMonitorSensorEntityDescription, + entry_id: str, argument: str = "", + legacy_enabled: bool = False, ) -> None: """Initialize the sensor.""" self.entity_description = sensor_description @@ -446,6 +554,13 @@ class SystemMonitorSensor(SensorEntity): self._attr_unique_id: str = slugify(f"{sensor_description.key}_{argument}") self._sensor_registry = sensor_registry self._argument: str = argument + self._attr_entity_registry_enabled_default = legacy_enabled + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="System Monitor", + name="System Monitor", + ) @property def native_value(self) -> str | datetime | None: diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json new file mode 100644 index 00000000000..88ecad4b107 --- /dev/null +++ b/homeassistant/components/systemmonitor/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "step": { + "user": { + "description": "Press submit for initial setup. On the created config entry, press configure to add sensors for selected processes" + } + } + }, + "options": { + "step": { + "init": { + "description": "Configure a monitoring sensor for a running process", + "data": { + "process": "Processes to add as sensor(s)" + }, + "data_description": { + "process": "Select a running process from the list or add a custom value. Multiple selections/custom values are supported" + } + } + } + } +} diff --git a/homeassistant/components/systemmonitor/util.py b/homeassistant/components/systemmonitor/util.py new file mode 100644 index 00000000000..bb81d0c9715 --- /dev/null +++ b/homeassistant/components/systemmonitor/util.py @@ -0,0 +1,42 @@ +"""Utils for System Monitor.""" + +import logging +import os + +import psutil + +_LOGGER = logging.getLogger(__name__) + + +def get_all_disk_mounts() -> list[str]: + """Return all disk mount points on system.""" + disks: list[str] = [] + for part in psutil.disk_partitions(all=False): + if os.name == "nt": + if "cdrom" in part.opts or part.fstype == "": + # skip cd-rom drives with no disk in it; they may raise + # ENOENT, pop-up a Windows GUI error for a non-ready + # partition or just hang. + continue + disks.append(part.mountpoint) + _LOGGER.debug("Adding disks: %s", ", ".join(disks)) + return disks + + +def get_all_network_interfaces() -> list[str]: + """Return all network interfaces on system.""" + interfaces: list[str] = [] + for interface, _ in psutil.net_if_addrs().items(): + interfaces.append(interface) + _LOGGER.debug("Adding interfaces: %s", ", ".join(interfaces)) + return interfaces + + +def get_all_running_processes() -> list[str]: + """Return all running processes on system.""" + processes: list[str] = [] + for proc in psutil.process_iter(["name"]): + if proc.name() not in processes: + processes.append(proc.name()) + _LOGGER.debug("Running processes: %s", ", ".join(processes)) + return processes diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dded0147422..d2674e128ce 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -490,6 +490,7 @@ FLOWS = { "syncthru", "synology_dsm", "system_bridge", + "systemmonitor", "tado", "tailscale", "tailwind", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d80f4f18925..d8ba63322ca 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5731,7 +5731,7 @@ "systemmonitor": { "name": "System Monitor", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_push" }, "tado": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51c872436aa..5007ed4262e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,6 +1176,9 @@ prometheus-client==0.17.1 # homeassistant.components.recorder psutil-home-assistant==0.0.1 +# homeassistant.components.systemmonitor +psutil==5.9.7 + # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 diff --git a/tests/components/systemmonitor/__init__.py b/tests/components/systemmonitor/__init__.py new file mode 100644 index 00000000000..92e60c1dbb2 --- /dev/null +++ b/tests/components/systemmonitor/__init__.py @@ -0,0 +1 @@ +"""Tests for the System Monitor component.""" diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py new file mode 100644 index 00000000000..ca21c971cf1 --- /dev/null +++ b/tests/components/systemmonitor/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for the System Monitor integration.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setup entry.""" + with patch( + "homeassistant.components.systemmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/systemmonitor/test_config_flow.py b/tests/components/systemmonitor/test_config_flow.py new file mode 100644 index 00000000000..367d38b91aa --- /dev/null +++ b/tests/components/systemmonitor/test_config_flow.py @@ -0,0 +1,270 @@ +"""Test the System Monitor config flow.""" +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN +from homeassistant.components.systemmonitor.const import CONF_PROCESS, DOMAIN +from homeassistant.const import Platform +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.util import slugify + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["options"] == {} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import( + hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry +) -> None: + """Test import.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "processes": ["systemd", "octave-cli"], + "legacy_resources": [ + "disk_use_percent_/", + "memory_free_", + "network_out_eth0", + "process_systemd", + "process_octave-cli", + ], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["options"] == { + "sensor": {"process": ["systemd", "octave-cli"]}, + "resources": [ + "disk_use_percent_/", + "memory_free_", + "network_out_eth0", + "process_systemd", + "process_octave-cli", + ], + } + + assert len(mock_setup_entry.mock_calls) == 1 + + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + assert issue.issue_domain == DOMAIN + assert issue.translation_placeholders == { + "domain": DOMAIN, + "integration_title": "System Monitor", + } + + +async def test_form_already_configured( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test abort when already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=config_entries.SOURCE_USER, + options={}, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_already_configured( + hass: HomeAssistant, mock_setup_entry: AsyncMock, issue_registry: ir.IssueRegistry +) -> None: + """Test abort when already configured for import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=config_entries.SOURCE_USER, + options={ + "sensor": [{CONF_PROCESS: "systemd"}], + "resources": [ + "disk_use_percent_/", + "memory_free_", + "network_out_eth0", + "process_systemd", + "process_octave-cli", + ], + }, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "processes": ["systemd", "octave-cli"], + "legacy_resources": [ + "disk_use_percent_/", + "memory_free_", + "network_out_eth0", + "process_systemd", + "process_octave-cli", + ], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + issue = issue_registry.async_get_issue( + HOMEASSISTANT_DOMAIN, f"deprecated_yaml_{DOMAIN}" + ) + assert issue.issue_domain == DOMAIN + assert issue.translation_placeholders == { + "domain": DOMAIN, + "integration_title": "System Monitor", + } + + +async def test_add_and_remove_processes( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test adding and removing process sensors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=config_entries.SOURCE_USER, + options={}, + entry_id="1", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PROCESS: ["systemd"], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "sensor": { + CONF_PROCESS: ["systemd"], + } + } + + # Add another + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PROCESS: ["systemd", "octave-cli"], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "sensor": { + CONF_PROCESS: ["systemd", "octave-cli"], + }, + } + + entity_reg = er.async_get(hass) + entity_reg.async_get_or_create( + domain=Platform.SENSOR, + platform=DOMAIN, + unique_id=slugify("process_systemd"), + config_entry=config_entry, + ) + entity_reg.async_get_or_create( + domain=Platform.SENSOR, + platform=DOMAIN, + unique_id=slugify("process_octave-cli"), + config_entry=config_entry, + ) + assert entity_reg.async_get("sensor.systemmonitor_process_systemd") is not None + assert entity_reg.async_get("sensor.systemmonitor_process_octave_cli") is not None + + # Remove one + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PROCESS: ["systemd"], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "sensor": { + CONF_PROCESS: ["systemd"], + }, + } + + # Remove last + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_PROCESS: [], + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + "sensor": {CONF_PROCESS: []}, + } + + assert entity_reg.async_get("sensor.systemmonitor_process_systemd") is None + assert entity_reg.async_get("sensor.systemmonitor_process_octave_cli") is None