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
This commit is contained in:
G Johansson 2023-12-26 18:29:32 +01:00 committed by GitHub
parent 2cd6c2b6bf
commit 4f0ee20ec5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 687 additions and 24 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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",
]

View file

@ -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"],

View file

@ -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(
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_TYPES[type_], argument)
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_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:

View file

@ -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"
}
}
}
}
}

View file

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

View file

@ -490,6 +490,7 @@ FLOWS = {
"syncthru",
"synology_dsm",
"system_bridge",
"systemmonitor",
"tado",
"tailscale",
"tailwind",

View file

@ -5731,7 +5731,7 @@
"systemmonitor": {
"name": "System Monitor",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_push"
},
"tado": {

View file

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

View file

@ -0,0 +1 @@
"""Tests for the System Monitor component."""

View file

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

View file

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