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:
parent
2cd6c2b6bf
commit
4f0ee20ec5
15 changed files with 687 additions and 24 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
143
homeassistant/components/systemmonitor/config_flow.py
Normal file
143
homeassistant/components/systemmonitor/config_flow.py
Normal 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)
|
17
homeassistant/components/systemmonitor/const.py
Normal file
17
homeassistant/components/systemmonitor/const.py
Normal 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",
|
||||
]
|
|
@ -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"],
|
||||
|
|
|
@ -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:
|
||||
|
|
25
homeassistant/components/systemmonitor/strings.json
Normal file
25
homeassistant/components/systemmonitor/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
homeassistant/components/systemmonitor/util.py
Normal file
42
homeassistant/components/systemmonitor/util.py
Normal 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
|
|
@ -490,6 +490,7 @@ FLOWS = {
|
|||
"syncthru",
|
||||
"synology_dsm",
|
||||
"system_bridge",
|
||||
"systemmonitor",
|
||||
"tado",
|
||||
"tailscale",
|
||||
"tailwind",
|
||||
|
|
|
@ -5731,7 +5731,7 @@
|
|||
"systemmonitor": {
|
||||
"name": "System Monitor",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"tado": {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
1
tests/components/systemmonitor/__init__.py
Normal file
1
tests/components/systemmonitor/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the System Monitor component."""
|
17
tests/components/systemmonitor/conftest.py
Normal file
17
tests/components/systemmonitor/conftest.py
Normal 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
|
270
tests/components/systemmonitor/test_config_flow.py
Normal file
270
tests/components/systemmonitor/test_config_flow.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue