mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
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
@ -1305,7 +1305,9 @@ omit =
|
|||||||
homeassistant/components/system_bridge/notify.py
|
homeassistant/components/system_bridge/notify.py
|
||||||
homeassistant/components/system_bridge/sensor.py
|
homeassistant/components/system_bridge/sensor.py
|
||||||
homeassistant/components/system_bridge/update.py
|
homeassistant/components/system_bridge/update.py
|
||||||
|
homeassistant/components/systemmonitor/__init__.py
|
||||||
homeassistant/components/systemmonitor/sensor.py
|
homeassistant/components/systemmonitor/sensor.py
|
||||||
|
homeassistant/components/systemmonitor/util.py
|
||||||
homeassistant/components/tado/__init__.py
|
homeassistant/components/tado/__init__.py
|
||||||
homeassistant/components/tado/binary_sensor.py
|
homeassistant/components/tado/binary_sensor.py
|
||||||
homeassistant/components/tado/climate.py
|
homeassistant/components/tado/climate.py
|
||||||
|
@ -1297,6 +1297,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/synology_srm/ @aerialls
|
/homeassistant/components/synology_srm/ @aerialls
|
||||||
/homeassistant/components/system_bridge/ @timmo001
|
/homeassistant/components/system_bridge/ @timmo001
|
||||||
/tests/components/system_bridge/ @timmo001
|
/tests/components/system_bridge/ @timmo001
|
||||||
|
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||||
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
||||||
/tests/components/tado/ @michaelarnauts @chiefdragon
|
/tests/components/tado/ @michaelarnauts @chiefdragon
|
||||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
/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",
|
"domain": "systemmonitor",
|
||||||
"name": "System Monitor",
|
"name": "System Monitor",
|
||||||
"codeowners": [],
|
"codeowners": ["@gjohansson-ST"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
|
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["psutil"],
|
"loggers": ["psutil"],
|
||||||
|
@ -15,26 +15,29 @@ import psutil
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
|
DOMAIN as SENSOR_DOMAIN,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_RESOURCES,
|
CONF_RESOURCES,
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_TYPE,
|
CONF_TYPE,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
|
EntityCategory,
|
||||||
UnitOfDataRate,
|
UnitOfDataRate,
|
||||||
UnitOfInformation,
|
UnitOfInformation,
|
||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect,
|
async_dispatcher_connect,
|
||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
@ -46,6 +49,9 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
import homeassistant.util.dt as dt_util
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ARG = "arg"
|
CONF_ARG = "arg"
|
||||||
@ -261,6 +267,17 @@ def check_required_arg(value: Any) -> Any:
|
|||||||
return value
|
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(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_RESOURCES, default={CONF_TYPE: "disk_use"}): vol.All(
|
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,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the system monitor sensors."""
|
"""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 = []
|
entities = []
|
||||||
sensor_registry: dict[tuple[str, str], SensorData] = {}
|
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]:
|
_LOGGER.debug("Setup from options %s", entry.options)
|
||||||
type_ = resource[CONF_TYPE]
|
|
||||||
# Initialize the sensor argument if none was provided.
|
for _type, sensor_description in SENSOR_TYPES.items():
|
||||||
# For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified.
|
if _type.startswith("disk_"):
|
||||||
if CONF_ARG not in resource:
|
for argument in disk_arguments:
|
||||||
argument = ""
|
sensor_registry[(_type, argument)] = SensorData(
|
||||||
if resource[CONF_TYPE].startswith("disk_"):
|
argument, None, None, None, None
|
||||||
argument = "/"
|
)
|
||||||
else:
|
is_enabled = check_legacy_resource(
|
||||||
argument = resource[CONF_ARG]
|
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.
|
# Verify if we can retrieve CPU / processor temperatures.
|
||||||
# If not, do not create the entity and add a warning to the log
|
# If not, do not create the entity and add a warning to the log
|
||||||
if (
|
if _type == "processor_temperature" and cpu_temperature is None:
|
||||||
type_ == "processor_temperature"
|
|
||||||
and await hass.async_add_executor_job(_read_cpu_temperature) is None
|
|
||||||
):
|
|
||||||
_LOGGER.warning("Cannot read CPU / processor temperature information")
|
_LOGGER.warning("Cannot read CPU / processor temperature information")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sensor_registry[(type_, argument)] = SensorData(
|
if _type == "process":
|
||||||
argument, None, None, None, None
|
_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(
|
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)
|
await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
@ -433,12 +537,16 @@ class SystemMonitorSensor(SensorEntity):
|
|||||||
"""Implementation of a system monitor sensor."""
|
"""Implementation of a system monitor sensor."""
|
||||||
|
|
||||||
should_poll = False
|
should_poll = False
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
sensor_registry: dict[tuple[str, str], SensorData],
|
sensor_registry: dict[tuple[str, str], SensorData],
|
||||||
sensor_description: SysMonitorSensorEntityDescription,
|
sensor_description: SysMonitorSensorEntityDescription,
|
||||||
|
entry_id: str,
|
||||||
argument: str = "",
|
argument: str = "",
|
||||||
|
legacy_enabled: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.entity_description = sensor_description
|
self.entity_description = sensor_description
|
||||||
@ -446,6 +554,13 @@ class SystemMonitorSensor(SensorEntity):
|
|||||||
self._attr_unique_id: str = slugify(f"{sensor_description.key}_{argument}")
|
self._attr_unique_id: str = slugify(f"{sensor_description.key}_{argument}")
|
||||||
self._sensor_registry = sensor_registry
|
self._sensor_registry = sensor_registry
|
||||||
self._argument: str = argument
|
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
|
@property
|
||||||
def native_value(self) -> str | datetime | None:
|
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",
|
"syncthru",
|
||||||
"synology_dsm",
|
"synology_dsm",
|
||||||
"system_bridge",
|
"system_bridge",
|
||||||
|
"systemmonitor",
|
||||||
"tado",
|
"tado",
|
||||||
"tailscale",
|
"tailscale",
|
||||||
"tailwind",
|
"tailwind",
|
||||||
|
@ -5731,7 +5731,7 @@
|
|||||||
"systemmonitor": {
|
"systemmonitor": {
|
||||||
"name": "System Monitor",
|
"name": "System Monitor",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"iot_class": "local_push"
|
"iot_class": "local_push"
|
||||||
},
|
},
|
||||||
"tado": {
|
"tado": {
|
||||||
|
@ -1176,6 +1176,9 @@ prometheus-client==0.17.1
|
|||||||
# homeassistant.components.recorder
|
# homeassistant.components.recorder
|
||||||
psutil-home-assistant==0.0.1
|
psutil-home-assistant==0.0.1
|
||||||
|
|
||||||
|
# homeassistant.components.systemmonitor
|
||||||
|
psutil==5.9.7
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
pure-python-adb[async]==0.3.0.dev0
|
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…
x
Reference in New Issue
Block a user