Create update coordinator for Systemmonitor (#106693)

This commit is contained in:
G Johansson 2024-01-17 02:07:55 +01:00 committed by GitHub
parent d5c1049bfe
commit d4f9ad9dd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 678 additions and 398 deletions

View file

@ -5,13 +5,37 @@ DOMAIN = "systemmonitor"
CONF_INDEX = "index" CONF_INDEX = "index"
CONF_PROCESS = "process" CONF_PROCESS = "process"
NETWORK_TYPES = [ NET_IO_TYPES = [
"network_in", "network_in",
"network_out", "network_out",
"throughput_network_in", "throughput_network_in",
"throughput_network_out", "throughput_network_out",
"packets_in", "packets_in",
"packets_out", "packets_out",
"ipv4_address", ]
"ipv6_address",
# There might be additional keys to be added for different
# platforms / hardware combinations.
# Taken from last version of "glances" integration before they moved to
# a generic temperature sensor logic.
# https://github.com/home-assistant/core/blob/5e15675593ba94a2c11f9f929cdad317e27ce190/homeassistant/components/glances/sensor.py#L199
CPU_SENSOR_PREFIXES = [
"amdgpu 1",
"aml_thermal",
"Core 0",
"Core 1",
"CPU Temperature",
"CPU",
"cpu-thermal 1",
"cpu_thermal 1",
"exynos-therm 1",
"Package id 0",
"Physical id 0",
"radeon 1",
"soc-thermal 1",
"soc_thermal 1",
"Tctl",
"cpu0-thermal",
"cpu0_thermal",
"k10temp 1",
] ]

View file

@ -0,0 +1,166 @@
"""DataUpdateCoordinators for the System monitor integration."""
from __future__ import annotations
from abc import abstractmethod
from datetime import datetime
import logging
import os
from typing import NamedTuple, TypeVar
import psutil
from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
class VirtualMemory(NamedTuple):
"""Represents virtual memory.
psutil defines virtual memory by platform.
Create our own definition here to be platform independent.
"""
total: float
available: float
percent: float
used: float
free: float
dataT = TypeVar(
"dataT",
bound=datetime
| dict[str, list[shwtemp]]
| dict[str, list[snicaddr]]
| dict[str, snetio]
| float
| list[psutil.Process]
| sswap
| VirtualMemory
| tuple[float, float, float]
| sdiskusage,
)
class MonitorCoordinator(DataUpdateCoordinator[dataT]):
"""A System monitor Base Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, name: str) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=f"System Monitor {name}",
update_interval=DEFAULT_SCAN_INTERVAL,
always_update=False,
)
async def _async_update_data(self) -> dataT:
"""Fetch data."""
return await self.hass.async_add_executor_job(self.update_data)
@abstractmethod
def update_data(self) -> dataT:
"""To be extended by data update coordinators."""
class SystemMonitorDiskCoordinator(MonitorCoordinator[sdiskusage]):
"""A System monitor Disk Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, name: str, argument: str) -> None:
"""Initialize the disk coordinator."""
super().__init__(hass, name)
self._argument = argument
def update_data(self) -> sdiskusage:
"""Fetch data."""
try:
return psutil.disk_usage(self._argument)
except PermissionError as err:
raise UpdateFailed(f"No permission to access {self._argument}") from err
except OSError as err:
raise UpdateFailed(f"OS error for {self._argument}") from err
class SystemMonitorSwapCoordinator(MonitorCoordinator[sswap]):
"""A System monitor Swap Data Update Coordinator."""
def update_data(self) -> sswap:
"""Fetch data."""
return psutil.swap_memory()
class SystemMonitorMemoryCoordinator(MonitorCoordinator[VirtualMemory]):
"""A System monitor Memory Data Update Coordinator."""
def update_data(self) -> VirtualMemory:
"""Fetch data."""
memory = psutil.virtual_memory()
return VirtualMemory(
memory.total, memory.available, memory.percent, memory.used, memory.free
)
class SystemMonitorNetIOCoordinator(MonitorCoordinator[dict[str, snetio]]):
"""A System monitor Network IO Data Update Coordinator."""
def update_data(self) -> dict[str, snetio]:
"""Fetch data."""
return psutil.net_io_counters(pernic=True)
class SystemMonitorNetAddrCoordinator(MonitorCoordinator[dict[str, list[snicaddr]]]):
"""A System monitor Network Address Data Update Coordinator."""
def update_data(self) -> dict[str, list[snicaddr]]:
"""Fetch data."""
return psutil.net_if_addrs()
class SystemMonitorLoadCoordinator(MonitorCoordinator[tuple[float, float, float]]):
"""A System monitor Load Data Update Coordinator."""
def update_data(self) -> tuple[float, float, float]:
"""Fetch data."""
return os.getloadavg()
class SystemMonitorProcessorCoordinator(MonitorCoordinator[float]):
"""A System monitor Processor Data Update Coordinator."""
def update_data(self) -> float:
"""Fetch data."""
return psutil.cpu_percent(interval=None)
class SystemMonitorBootTimeCoordinator(MonitorCoordinator[datetime]):
"""A System monitor Processor Data Update Coordinator."""
def update_data(self) -> datetime:
"""Fetch data."""
return dt_util.utc_from_timestamp(psutil.boot_time())
class SystemMonitorProcessCoordinator(MonitorCoordinator[list[psutil.Process]]):
"""A System monitor Process Data Update Coordinator."""
def update_data(self) -> list[psutil.Process]:
"""Fetch data."""
processes = psutil.process_iter()
return list(processes)
class SystemMonitorCPUtempCoordinator(MonitorCoordinator[dict[str, list[shwtemp]]]):
"""A System monitor CPU Temperature Data Update Coordinator."""
def update_data(self) -> dict[str, list[shwtemp]]:
"""Fetch data."""
try:
return psutil.sensors_temperatures()
except AttributeError as err:
raise UpdateFailed("OS does not provide temperature sensors") from err

View file

@ -1,17 +1,18 @@
"""Support for monitoring the local system.""" """Support for monitoring the local system."""
from __future__ import annotations from __future__ import annotations
import asyncio from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime
from functools import cache, lru_cache from functools import lru_cache
import logging import logging
import os
import socket import socket
import sys import sys
from typing import Any, Literal import time
from typing import Any, Generic, Literal
import psutil import psutil
from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -26,7 +27,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_RESOURCES, CONF_RESOURCES,
CONF_TYPE, CONF_TYPE,
EVENT_HOMEASSISTANT_STOP,
PERCENTAGE, PERCENTAGE,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
@ -35,22 +35,31 @@ from homeassistant.const import (
UnitOfInformation, UnitOfInformation,
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
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.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
from .const import CONF_PROCESS, DOMAIN, NETWORK_TYPES from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES
from .util import get_all_disk_mounts, get_all_network_interfaces from .coordinator import (
MonitorCoordinator,
SystemMonitorBootTimeCoordinator,
SystemMonitorCPUtempCoordinator,
SystemMonitorDiskCoordinator,
SystemMonitorLoadCoordinator,
SystemMonitorMemoryCoordinator,
SystemMonitorNetAddrCoordinator,
SystemMonitorNetIOCoordinator,
SystemMonitorProcessCoordinator,
SystemMonitorProcessorCoordinator,
SystemMonitorSwapCoordinator,
VirtualMemory,
dataT,
)
from .util import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,16 +83,92 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]:
return "mdi:cpu-32-bit" return "mdi:cpu-32-bit"
@dataclass(frozen=True) def get_processor_temperature(
class SysMonitorSensorEntityDescription(SensorEntityDescription): entity: SystemMonitorSensor[dict[str, list[shwtemp]]],
"""Description for System Monitor sensor entities.""" ) -> float | None:
"""Return processor temperature."""
return read_cpu_temperature(entity.coordinator.data)
def get_process(entity: SystemMonitorSensor[list[psutil.Process]]) -> str:
"""Return process."""
state = STATE_OFF
for proc in entity.coordinator.data:
try:
_LOGGER.debug("process %s for argument %s", proc.name(), entity.argument)
if entity.argument == proc.name():
state = STATE_ON
break
except psutil.NoSuchProcess as err:
_LOGGER.warning(
"Failed to load process with ID: %s, old name: %s",
err.pid,
err.name,
)
return state
def get_network(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None:
"""Return network in and out."""
counters = entity.coordinator.data
if entity.argument in counters:
counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
return round(counter / 1024**2, 1)
return None
def get_packets(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None:
"""Return packets in and out."""
counters = entity.coordinator.data
if entity.argument in counters:
return counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
return None
def get_throughput(entity: SystemMonitorSensor[dict[str, snetio]]) -> float | None:
"""Return network throughput in and out."""
counters = entity.coordinator.data
state = None
if entity.argument in counters:
counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
now = time.monotonic()
if (
(value := entity.value)
and (update_time := entity.update_time)
and value < counter
):
state = round(
(counter - value) / 1000**2 / (now - update_time),
3,
)
entity.update_time = now
entity.value = counter
return state
def get_ip_address(
entity: SystemMonitorSensor[dict[str, list[snicaddr]]],
) -> str | None:
"""Return network ip address."""
addresses = entity.coordinator.data
if entity.argument in addresses:
for addr in addresses[entity.argument]:
if addr.family == IF_ADDRS_FAMILY[entity.entity_description.key]:
return addr.address
return None
@dataclass(frozen=True, kw_only=True)
class SysMonitorSensorEntityDescription(SensorEntityDescription, Generic[dataT]):
"""Describes System Monitor sensor entities."""
value_fn: Callable[[SystemMonitorSensor[dataT]], StateType | datetime]
mandatory_arg: bool = False mandatory_arg: bool = False
placeholder: str | None = None placeholder: str | None = None
SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = {
"disk_free": SysMonitorSensorEntityDescription( "disk_free": SysMonitorSensorEntityDescription[sdiskusage](
key="disk_free", key="disk_free",
translation_key="disk_free", translation_key="disk_free",
placeholder="mount_point", placeholder="mount_point",
@ -91,8 +176,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data.free / 1024**3, 1),
), ),
"disk_use": SysMonitorSensorEntityDescription( "disk_use": SysMonitorSensorEntityDescription[sdiskusage](
key="disk_use", key="disk_use",
translation_key="disk_use", translation_key="disk_use",
placeholder="mount_point", placeholder="mount_point",
@ -100,76 +186,91 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data.used / 1024**3, 1),
), ),
"disk_use_percent": SysMonitorSensorEntityDescription( "disk_use_percent": SysMonitorSensorEntityDescription[sdiskusage](
key="disk_use_percent", key="disk_use_percent",
translation_key="disk_use_percent", translation_key="disk_use_percent",
placeholder="mount_point", placeholder="mount_point",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: entity.coordinator.data.percent,
), ),
"ipv4_address": SysMonitorSensorEntityDescription( "ipv4_address": SysMonitorSensorEntityDescription[dict[str, list[snicaddr]]](
key="ipv4_address", key="ipv4_address",
translation_key="ipv4_address", translation_key="ipv4_address",
placeholder="ip_address", placeholder="ip_address",
icon="mdi:ip-network", icon="mdi:ip-network",
mandatory_arg=True, mandatory_arg=True,
value_fn=get_ip_address,
), ),
"ipv6_address": SysMonitorSensorEntityDescription( "ipv6_address": SysMonitorSensorEntityDescription[dict[str, list[snicaddr]]](
key="ipv6_address", key="ipv6_address",
translation_key="ipv6_address", translation_key="ipv6_address",
placeholder="ip_address", placeholder="ip_address",
icon="mdi:ip-network", icon="mdi:ip-network",
mandatory_arg=True, mandatory_arg=True,
value_fn=get_ip_address,
), ),
"last_boot": SysMonitorSensorEntityDescription( "last_boot": SysMonitorSensorEntityDescription[datetime](
key="last_boot", key="last_boot",
translation_key="last_boot", translation_key="last_boot",
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda entity: entity.coordinator.data,
), ),
"load_15m": SysMonitorSensorEntityDescription( "load_15m": SysMonitorSensorEntityDescription[tuple[float, float, float]](
key="load_15m", key="load_15m",
translation_key="load_15m", translation_key="load_15m",
icon=get_cpu_icon(), icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data[2], 2),
), ),
"load_1m": SysMonitorSensorEntityDescription( "load_1m": SysMonitorSensorEntityDescription[tuple[float, float, float]](
key="load_1m", key="load_1m",
translation_key="load_1m", translation_key="load_1m",
icon=get_cpu_icon(), icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data[0], 2),
), ),
"load_5m": SysMonitorSensorEntityDescription( "load_5m": SysMonitorSensorEntityDescription[tuple[float, float, float]](
key="load_5m", key="load_5m",
translation_key="load_5m", translation_key="load_5m",
icon=get_cpu_icon(), icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data[1], 2),
), ),
"memory_free": SysMonitorSensorEntityDescription( "memory_free": SysMonitorSensorEntityDescription[VirtualMemory](
key="memory_free", key="memory_free",
translation_key="memory_free", translation_key="memory_free",
native_unit_of_measurement=UnitOfInformation.MEBIBYTES, native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:memory", icon="mdi:memory",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data.available / 1024**2, 1),
), ),
"memory_use": SysMonitorSensorEntityDescription( "memory_use": SysMonitorSensorEntityDescription[VirtualMemory](
key="memory_use", key="memory_use",
translation_key="memory_use", translation_key="memory_use",
native_unit_of_measurement=UnitOfInformation.MEBIBYTES, native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:memory", icon="mdi:memory",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(
(entity.coordinator.data.total - entity.coordinator.data.available)
/ 1024**2,
1,
), ),
"memory_use_percent": SysMonitorSensorEntityDescription( ),
"memory_use_percent": SysMonitorSensorEntityDescription[VirtualMemory](
key="memory_use_percent", key="memory_use_percent",
translation_key="memory_use_percent", translation_key="memory_use_percent",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:memory", icon="mdi:memory",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: entity.coordinator.data.percent,
), ),
"network_in": SysMonitorSensorEntityDescription( "network_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="network_in", key="network_in",
translation_key="network_in", translation_key="network_in",
placeholder="interface", placeholder="interface",
@ -178,8 +279,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
icon="mdi:server-network", icon="mdi:server-network",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_network,
), ),
"network_out": SysMonitorSensorEntityDescription( "network_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="network_out", key="network_out",
translation_key="network_out", translation_key="network_out",
placeholder="interface", placeholder="interface",
@ -188,24 +290,27 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
icon="mdi:server-network", icon="mdi:server-network",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_network,
), ),
"packets_in": SysMonitorSensorEntityDescription( "packets_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="packets_in", key="packets_in",
translation_key="packets_in", translation_key="packets_in",
placeholder="interface", placeholder="interface",
icon="mdi:server-network", icon="mdi:server-network",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_packets,
), ),
"packets_out": SysMonitorSensorEntityDescription( "packets_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="packets_out", key="packets_out",
translation_key="packets_out", translation_key="packets_out",
placeholder="interface", placeholder="interface",
icon="mdi:server-network", icon="mdi:server-network",
state_class=SensorStateClass.TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_packets,
), ),
"throughput_network_in": SysMonitorSensorEntityDescription( "throughput_network_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="throughput_network_in", key="throughput_network_in",
translation_key="throughput_network_in", translation_key="throughput_network_in",
placeholder="interface", placeholder="interface",
@ -213,8 +318,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_throughput,
), ),
"throughput_network_out": SysMonitorSensorEntityDescription( "throughput_network_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
key="throughput_network_out", key="throughput_network_out",
translation_key="throughput_network_out", translation_key="throughput_network_out",
placeholder="interface", placeholder="interface",
@ -222,50 +328,59 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
device_class=SensorDeviceClass.DATA_RATE, device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
mandatory_arg=True, mandatory_arg=True,
value_fn=get_throughput,
), ),
"process": SysMonitorSensorEntityDescription( "process": SysMonitorSensorEntityDescription[list[psutil.Process]](
key="process", key="process",
translation_key="process", translation_key="process",
placeholder="process", placeholder="process",
icon=get_cpu_icon(), icon=get_cpu_icon(),
mandatory_arg=True, mandatory_arg=True,
value_fn=get_process,
), ),
"processor_use": SysMonitorSensorEntityDescription( "processor_use": SysMonitorSensorEntityDescription[float](
key="processor_use", key="processor_use",
translation_key="processor_use", translation_key="processor_use",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon=get_cpu_icon(), icon=get_cpu_icon(),
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data),
), ),
"processor_temperature": SysMonitorSensorEntityDescription( "processor_temperature": SysMonitorSensorEntityDescription[
dict[str, list[shwtemp]]
](
key="processor_temperature", key="processor_temperature",
translation_key="processor_temperature", translation_key="processor_temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=get_processor_temperature,
), ),
"swap_free": SysMonitorSensorEntityDescription( "swap_free": SysMonitorSensorEntityDescription[sswap](
key="swap_free", key="swap_free",
translation_key="swap_free", translation_key="swap_free",
native_unit_of_measurement=UnitOfInformation.MEBIBYTES, native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data.free / 1024**2, 1),
), ),
"swap_use": SysMonitorSensorEntityDescription( "swap_use": SysMonitorSensorEntityDescription[sswap](
key="swap_use", key="swap_use",
translation_key="swap_use", translation_key="swap_use",
native_unit_of_measurement=UnitOfInformation.MEBIBYTES, native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
device_class=SensorDeviceClass.DATA_SIZE, device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(entity.coordinator.data.used / 1024**2, 1),
), ),
"swap_use_percent": SysMonitorSensorEntityDescription( "swap_use_percent": SysMonitorSensorEntityDescription[sswap](
key="swap_use_percent", key="swap_use_percent",
translation_key="swap_use_percent", translation_key="swap_use_percent",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk", icon="mdi:harddisk",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: entity.coordinator.data.percent,
), ),
} }
@ -320,46 +435,8 @@ IO_COUNTER = {
"throughput_network_out": 0, "throughput_network_out": 0,
"throughput_network_in": 1, "throughput_network_in": 1,
} }
IF_ADDRS_FAMILY = {"ipv4_address": socket.AF_INET, "ipv6_address": socket.AF_INET6} IF_ADDRS_FAMILY = {"ipv4_address": socket.AF_INET, "ipv6_address": socket.AF_INET6}
# There might be additional keys to be added for different
# platforms / hardware combinations.
# Taken from last version of "glances" integration before they moved to
# a generic temperature sensor logic.
# https://github.com/home-assistant/core/blob/5e15675593ba94a2c11f9f929cdad317e27ce190/homeassistant/components/glances/sensor.py#L199
CPU_SENSOR_PREFIXES = [
"amdgpu 1",
"aml_thermal",
"Core 0",
"Core 1",
"CPU Temperature",
"CPU",
"cpu-thermal 1",
"cpu_thermal 1",
"exynos-therm 1",
"Package id 0",
"Physical id 0",
"radeon 1",
"soc-thermal 1",
"soc_thermal 1",
"Tctl",
"cpu0-thermal",
"cpu0_thermal",
"k10temp 1",
]
@dataclass
class SensorData:
"""Data for a sensor."""
argument: Any
state: str | datetime | None
value: Any | None
update_time: datetime | None
last_exception: BaseException | None
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -399,33 +476,69 @@ async def async_setup_platform(
) )
async def async_setup_entry( async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up System Montor sensors based on a config entry.""" """Set up System Montor sensors based on a config entry."""
entities = [] entities: list[SystemMonitorSensor] = []
sensor_registry: dict[tuple[str, str], SensorData] = {}
legacy_resources: set[str] = set(entry.options.get("resources", [])) legacy_resources: set[str] = set(entry.options.get("resources", []))
loaded_resources: set[str] = set() loaded_resources: set[str] = set()
disk_arguments = await hass.async_add_executor_job(get_all_disk_mounts)
network_arguments = await hass.async_add_executor_job(get_all_network_interfaces) def get_arguments() -> dict[str, Any]:
cpu_temperature = await hass.async_add_executor_job(_read_cpu_temperature) """Return startup information."""
disk_arguments = get_all_disk_mounts()
network_arguments = get_all_network_interfaces()
cpu_temperature = read_cpu_temperature()
return {
"disk_arguments": disk_arguments,
"network_arguments": network_arguments,
"cpu_temperature": cpu_temperature,
}
startup_arguments = await hass.async_add_executor_job(get_arguments)
disk_coordinators: dict[str, SystemMonitorDiskCoordinator] = {}
for argument in startup_arguments["disk_arguments"]:
disk_coordinators[argument] = SystemMonitorDiskCoordinator(
hass, f"Disk {argument} coordinator", argument
)
swap_coordinator = SystemMonitorSwapCoordinator(hass, "Swap coordinator")
memory_coordinator = SystemMonitorMemoryCoordinator(hass, "Memory coordinator")
net_io_coordinator = SystemMonitorNetIOCoordinator(hass, "Net IO coordnator")
net_addr_coordinator = SystemMonitorNetAddrCoordinator(
hass, "Net address coordinator"
)
system_load_coordinator = SystemMonitorLoadCoordinator(
hass, "System load coordinator"
)
processor_coordinator = SystemMonitorProcessorCoordinator(
hass, "Processor coordinator"
)
boot_time_coordinator = SystemMonitorBootTimeCoordinator(
hass, "Boot time coordinator"
)
process_coordinator = SystemMonitorProcessCoordinator(hass, "Process coordinator")
cpu_temp_coordinator = SystemMonitorCPUtempCoordinator(
hass, "CPU temperature coordinator"
)
for argument in startup_arguments["disk_arguments"]:
disk_coordinators[argument] = SystemMonitorDiskCoordinator(
hass, f"Disk {argument} coordinator", argument
)
_LOGGER.debug("Setup from options %s", entry.options) _LOGGER.debug("Setup from options %s", entry.options)
for _type, sensor_description in SENSOR_TYPES.items(): for _type, sensor_description in SENSOR_TYPES.items():
if _type.startswith("disk_"): if _type.startswith("disk_"):
for argument in disk_arguments: for argument in startup_arguments["disk_arguments"]:
sensor_registry[(_type, argument)] = SensorData(
argument, None, None, None, None
)
is_enabled = check_legacy_resource( is_enabled = check_legacy_resource(
f"{_type}_{argument}", legacy_resources f"{_type}_{argument}", legacy_resources
) )
loaded_resources.add(slugify(f"{_type}_{argument}")) loaded_resources.add(slugify(f"{_type}_{argument}"))
entities.append( entities.append(
SystemMonitorSensor( SystemMonitorSensor(
sensor_registry, disk_coordinators[argument],
sensor_description, sensor_description,
entry.entry_id, entry.entry_id,
argument, argument,
@ -434,18 +547,15 @@ async def async_setup_entry(
) )
continue continue
if _type in NETWORK_TYPES: if _type.startswith("ipv"):
for argument in network_arguments: for argument in startup_arguments["network_arguments"]:
sensor_registry[(_type, argument)] = SensorData(
argument, None, None, None, None
)
is_enabled = check_legacy_resource( is_enabled = check_legacy_resource(
f"{_type}_{argument}", legacy_resources f"{_type}_{argument}", legacy_resources
) )
loaded_resources.add(slugify(f"{_type}_{argument}")) loaded_resources.add(f"{_type}_{argument}")
entities.append( entities.append(
SystemMonitorSensor( SystemMonitorSensor(
sensor_registry, net_addr_coordinator,
sensor_description, sensor_description,
entry.entry_id, entry.entry_id,
argument, argument,
@ -454,22 +564,74 @@ async def async_setup_entry(
) )
continue continue
# Verify if we can retrieve CPU / processor temperatures. if _type == "last_boot":
# If not, do not create the entity and add a warning to the log argument = ""
if _type == "processor_temperature" and cpu_temperature is None: is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
_LOGGER.warning("Cannot read CPU / processor temperature information") loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
boot_time_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled,
)
)
continue
if _type.startswith("load_"):
argument = ""
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
system_load_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled,
)
)
continue
if _type.startswith("memory_"):
argument = ""
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
memory_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled,
)
)
if _type in NET_IO_TYPES:
for argument in startup_arguments["network_arguments"]:
is_enabled = check_legacy_resource(
f"{_type}_{argument}", legacy_resources
)
loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
net_io_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled,
)
)
continue continue
if _type == "process": if _type == "process":
_entry: dict[str, list] = entry.options.get(SENSOR_DOMAIN, {}) _entry = entry.options.get(SENSOR_DOMAIN, {})
for argument in _entry.get(CONF_PROCESS, []): for argument in _entry.get(CONF_PROCESS, []):
sensor_registry[(_type, argument)] = SensorData(
argument, None, None, None, None
)
loaded_resources.add(slugify(f"{_type}_{argument}")) loaded_resources.add(slugify(f"{_type}_{argument}"))
entities.append( entities.append(
SystemMonitorSensor( SystemMonitorSensor(
sensor_registry, process_coordinator,
sensor_description, sensor_description,
entry.entry_id, entry.entry_id,
argument, argument,
@ -478,15 +640,49 @@ async def async_setup_entry(
) )
continue continue
sensor_registry[(_type, "")] = SensorData("", None, None, None, None) if _type == "processor_use":
is_enabled = check_legacy_resource(f"{_type}_", legacy_resources) argument = ""
loaded_resources.add(f"{_type}_") is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
loaded_resources.add(f"{_type}_{argument}")
entities.append( entities.append(
SystemMonitorSensor( SystemMonitorSensor(
sensor_registry, processor_coordinator,
sensor_description, sensor_description,
entry.entry_id, entry.entry_id,
"", argument,
is_enabled,
)
)
continue
if _type == "processor_temperature":
if not startup_arguments["cpu_temperature"]:
# Don't load processor temperature sensor if we can't read it.
continue
argument = ""
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
cpu_temp_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled,
)
)
continue
if _type.startswith("swap_"):
argument = ""
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
loaded_resources.add(f"{_type}_{argument}")
entities.append(
SystemMonitorSensor(
swap_coordinator,
sensor_description,
entry.entry_id,
argument,
is_enabled, is_enabled,
) )
) )
@ -506,12 +702,13 @@ async def async_setup_entry(
_type = resource[:split_index] _type = resource[:split_index]
argument = resource[split_index + 1 :] argument = resource[split_index + 1 :]
_LOGGER.debug("Loading legacy %s with argument %s", _type, argument) _LOGGER.debug("Loading legacy %s with argument %s", _type, argument)
sensor_registry[(_type, argument)] = SensorData( if not disk_coordinators.get(argument):
argument, None, None, None, None disk_coordinators[argument] = SystemMonitorDiskCoordinator(
hass, f"Disk {argument} coordinator", argument
) )
entities.append( entities.append(
SystemMonitorSensor( SystemMonitorSensor(
sensor_registry, disk_coordinators[argument],
SENSOR_TYPES[_type], SENSOR_TYPES[_type],
entry.entry_id, entry.entry_id,
argument, argument,
@ -519,94 +716,45 @@ async def async_setup_entry(
) )
) )
scan_interval = DEFAULT_SCAN_INTERVAL # No gathering to avoid swamping the executor
await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval) for coordinator in disk_coordinators.values():
await coordinator.async_request_refresh()
await boot_time_coordinator.async_request_refresh()
await cpu_temp_coordinator.async_request_refresh()
await memory_coordinator.async_request_refresh()
await net_addr_coordinator.async_request_refresh()
await net_io_coordinator.async_request_refresh()
await process_coordinator.async_request_refresh()
await processor_coordinator.async_request_refresh()
await swap_coordinator.async_request_refresh()
await system_load_coordinator.async_request_refresh()
async_add_entities(entities) async_add_entities(entities)
async def async_setup_sensor_registry_updates( class SystemMonitorSensor(CoordinatorEntity[MonitorCoordinator[dataT]], SensorEntity):
hass: HomeAssistant,
sensor_registry: dict[tuple[str, str], SensorData],
scan_interval: timedelta,
) -> None:
"""Update the registry and create polling."""
_update_lock = asyncio.Lock()
def _update_sensors() -> None:
"""Update sensors and store the result in the registry."""
for (type_, argument), data in sensor_registry.items():
try:
state, value, update_time = _update(type_, data)
except Exception as ex: # pylint: disable=broad-except
_LOGGER.exception("Error updating sensor: %s (%s)", type_, argument)
data.last_exception = ex
else:
data.state = state
data.value = value
data.update_time = update_time
data.last_exception = None
# Only fetch these once per iteration as we use the same
# data source multiple times in _update
_disk_usage.cache_clear()
_swap_memory.cache_clear()
_virtual_memory.cache_clear()
_net_io_counters.cache_clear()
_net_if_addrs.cache_clear()
_getloadavg.cache_clear()
async def _async_update_data(*_: Any) -> None:
"""Update all sensors in one executor jump."""
if _update_lock.locked():
_LOGGER.warning(
(
"Updating systemmonitor took longer than the scheduled update"
" interval %s"
),
scan_interval,
)
return
async with _update_lock:
await hass.async_add_executor_job(_update_sensors)
async_dispatcher_send(hass, SIGNAL_SYSTEMMONITOR_UPDATE)
polling_remover = async_track_time_interval(hass, _async_update_data, scan_interval)
@callback
def _async_stop_polling(*_: Any) -> None:
polling_remover()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_polling)
await _async_update_data()
class SystemMonitorSensor(SensorEntity):
"""Implementation of a system monitor sensor.""" """Implementation of a system monitor sensor."""
should_poll = False
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: SysMonitorSensorEntityDescription
def __init__( def __init__(
self, self,
sensor_registry: dict[tuple[str, str], SensorData], coordinator: MonitorCoordinator,
sensor_description: SysMonitorSensorEntityDescription, sensor_description: SysMonitorSensorEntityDescription,
entry_id: str, entry_id: str,
argument: str = "", argument: str,
legacy_enabled: bool = False, legacy_enabled: bool = False,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = sensor_description self.entity_description = sensor_description
if self.entity_description.placeholder: if self.entity_description.placeholder:
self._attr_translation_placeholders = { self._attr_translation_placeholders = {
self.entity_description.placeholder: argument self.entity_description.placeholder: argument
} }
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._argument: str = argument
self._attr_entity_registry_enabled_default = legacy_enabled self._attr_entity_registry_enabled_default = legacy_enabled
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
@ -614,177 +762,11 @@ class SystemMonitorSensor(SensorEntity):
manufacturer="System Monitor", manufacturer="System Monitor",
name="System Monitor", name="System Monitor",
) )
self.argument = argument
self.value: int | None = None
self.update_time: float | None = None
@property @property
def native_value(self) -> str | datetime | None: def native_value(self) -> StateType | datetime:
"""Return the state of the device.""" """Return the state."""
return self.data.state return self.entity_description.value_fn(self)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.data.last_exception is None
@property
def data(self) -> SensorData:
"""Return registry entry for the data."""
return self._sensor_registry[(self.entity_description.key, self._argument)]
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_SYSTEMMONITOR_UPDATE, self.async_write_ha_state
)
)
def _update( # noqa: C901
type_: str, data: SensorData
) -> tuple[str | datetime | None, str | None, datetime | None]:
"""Get the latest system information."""
state = None
value = None
update_time = None
if type_ == "disk_use_percent":
state = _disk_usage(data.argument).percent
elif type_ == "disk_use":
state = round(_disk_usage(data.argument).used / 1024**3, 1)
elif type_ == "disk_free":
state = round(_disk_usage(data.argument).free / 1024**3, 1)
elif type_ == "memory_use_percent":
state = _virtual_memory().percent
elif type_ == "memory_use":
virtual_memory = _virtual_memory()
state = round((virtual_memory.total - virtual_memory.available) / 1024**2, 1)
elif type_ == "memory_free":
state = round(_virtual_memory().available / 1024**2, 1)
elif type_ == "swap_use_percent":
state = _swap_memory().percent
elif type_ == "swap_use":
state = round(_swap_memory().used / 1024**2, 1)
elif type_ == "swap_free":
state = round(_swap_memory().free / 1024**2, 1)
elif type_ == "processor_use":
state = round(psutil.cpu_percent(interval=None))
elif type_ == "processor_temperature":
state = _read_cpu_temperature()
elif type_ == "process":
state = STATE_OFF
for proc in psutil.process_iter():
try:
if data.argument == proc.name():
state = STATE_ON
break
except psutil.NoSuchProcess as err:
_LOGGER.warning(
"Failed to load process with ID: %s, old name: %s",
err.pid,
err.name,
)
elif type_ in ("network_out", "network_in"):
counters = _net_io_counters()
if data.argument in counters:
counter = counters[data.argument][IO_COUNTER[type_]]
state = round(counter / 1024**2, 1)
else:
state = None
elif type_ in ("packets_out", "packets_in"):
counters = _net_io_counters()
if data.argument in counters:
state = counters[data.argument][IO_COUNTER[type_]]
else:
state = None
elif type_ in ("throughput_network_out", "throughput_network_in"):
counters = _net_io_counters()
if data.argument in counters:
counter = counters[data.argument][IO_COUNTER[type_]]
now = dt_util.utcnow()
if data.value and data.value < counter:
state = round(
(counter - data.value)
/ 1000**2
/ (now - (data.update_time or now)).total_seconds(),
3,
)
else:
state = None
update_time = now
value = counter
else:
state = None
elif type_ in ("ipv4_address", "ipv6_address"):
addresses = _net_if_addrs()
if data.argument in addresses:
for addr in addresses[data.argument]:
if addr.family == IF_ADDRS_FAMILY[type_]:
state = addr.address
else:
state = None
elif type_ == "last_boot":
# Only update on initial setup
if data.state is None:
state = dt_util.utc_from_timestamp(psutil.boot_time())
else:
state = data.state
elif type_ == "load_1m":
state = round(_getloadavg()[0], 2)
elif type_ == "load_5m":
state = round(_getloadavg()[1], 2)
elif type_ == "load_15m":
state = round(_getloadavg()[2], 2)
return state, value, update_time
@cache
def _disk_usage(path: str) -> Any:
return psutil.disk_usage(path)
@cache
def _swap_memory() -> Any:
return psutil.swap_memory()
@cache
def _virtual_memory() -> Any:
return psutil.virtual_memory()
@cache
def _net_io_counters() -> Any:
return psutil.net_io_counters(pernic=True)
@cache
def _net_if_addrs() -> Any:
return psutil.net_if_addrs()
@cache
def _getloadavg() -> tuple[float, float, float]:
return os.getloadavg()
def _read_cpu_temperature() -> float | None:
"""Attempt to read CPU / processor temperature."""
try:
temps = psutil.sensors_temperatures()
except AttributeError:
# Linux, macOS
return None
for name, entries in temps.items():
for i, entry in enumerate(entries, start=1):
# In case the label is empty (e.g. on Raspberry PI 4),
# construct it ourself here based on the sensor key name.
_label = f"{name} {i}" if not entry.label else entry.label
# check both name and label because some systems embed cpu# in the
# name, which makes label not match because label adds cpu# at end.
if _label in CPU_SENSOR_PREFIXES or name in CPU_SENSOR_PREFIXES:
return round(entry.current, 1)
return None

View file

@ -1,9 +1,11 @@
"""Utils for System Monitor.""" """Utils for System Monitor."""
import logging import logging
import os import os
import psutil import psutil
from psutil._common import shwtemp
from .const import CPU_SENSOR_PREFIXES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -61,3 +63,22 @@ def get_all_running_processes() -> set[str]:
processes.add(proc.name()) processes.add(proc.name())
_LOGGER.debug("Running processes: %s", ", ".join(processes)) _LOGGER.debug("Running processes: %s", ", ".join(processes))
return processes return processes
def read_cpu_temperature(temps: dict[str, list[shwtemp]] | None = None) -> float | None:
"""Attempt to read CPU / processor temperature."""
if not temps:
temps = psutil.sensors_temperatures()
entry: shwtemp
for name, entries in temps.items():
for i, entry in enumerate(entries, start=1):
# In case the label is empty (e.g. on Raspberry PI 4),
# construct it ourself here based on the sensor key name.
_label = f"{name} {i}" if not entry.label else entry.label
# check both name and label because some systems embed cpu# in the
# name, which makes label not match because label adds cpu# at end.
if _label in CPU_SENSOR_PREFIXES or name in CPU_SENSOR_PREFIXES:
return round(entry.current, 1)
return None

View file

@ -115,11 +115,11 @@ def mock_process() -> list[MockProcess]:
def mock_psutil(mock_process: list[MockProcess]) -> Mock: def mock_psutil(mock_process: list[MockProcess]) -> Mock:
"""Mock psutil.""" """Mock psutil."""
with patch( with patch(
"homeassistant.components.systemmonitor.sensor.psutil", "homeassistant.components.systemmonitor.coordinator.psutil",
autospec=True, autospec=True,
) as mock_psutil: ) as mock_psutil:
mock_psutil.disk_usage.return_value = sdiskusage( mock_psutil.disk_usage.return_value = sdiskusage(
500 * 1024**2, 300 * 1024**2, 200 * 1024**2, 60.0 500 * 1024**3, 300 * 1024**3, 200 * 1024**3, 60.0
) )
mock_psutil.swap_memory.return_value = sswap( mock_psutil.swap_memory.return_value = sswap(
100 * 1024**2, 60 * 1024**2, 40 * 1024**2, 60.0, 1, 1 100 * 1024**2, 60 * 1024**2, 40 * 1024**2, 60.0, 1, 1
@ -240,7 +240,9 @@ def mock_util(mock_process) -> Mock:
@pytest.fixture @pytest.fixture
def mock_os() -> Mock: def mock_os() -> Mock:
"""Mock os.""" """Mock os."""
with patch("homeassistant.components.systemmonitor.sensor.os") as mock_os, patch( with patch(
"homeassistant.components.systemmonitor.coordinator.os"
) as mock_os, patch(
"homeassistant.components.systemmonitor.util.os" "homeassistant.components.systemmonitor.util.os"
) as mock_os_util: ) as mock_os_util:
mock_os_util.name = "nt" mock_os_util.name = "nt"

View file

@ -9,7 +9,7 @@
}) })
# --- # ---
# name: test_sensor[System Monitor Disk free / - state] # name: test_sensor[System Monitor Disk free / - state]
'0.2' '200.0'
# --- # ---
# name: test_sensor[System Monitor Disk free /media/share - attributes] # name: test_sensor[System Monitor Disk free /media/share - attributes]
ReadOnlyDict({ ReadOnlyDict({
@ -21,7 +21,7 @@
}) })
# --- # ---
# name: test_sensor[System Monitor Disk free /media/share - state] # name: test_sensor[System Monitor Disk free /media/share - state]
'0.2' '200.0'
# --- # ---
# name: test_sensor[System Monitor Disk usage / - attributes] # name: test_sensor[System Monitor Disk usage / - attributes]
ReadOnlyDict({ ReadOnlyDict({
@ -66,7 +66,7 @@
}) })
# --- # ---
# name: test_sensor[System Monitor Disk use / - state] # name: test_sensor[System Monitor Disk use / - state]
'0.3' '300.0'
# --- # ---
# name: test_sensor[System Monitor Disk use /media/share - attributes] # name: test_sensor[System Monitor Disk use /media/share - attributes]
ReadOnlyDict({ ReadOnlyDict({
@ -78,7 +78,7 @@
}) })
# --- # ---
# name: test_sensor[System Monitor Disk use /media/share - state] # name: test_sensor[System Monitor Disk use /media/share - state]
'0.3' '300.0'
# --- # ---
# name: test_sensor[System Monitor IPv4 address eth0 - attributes] # name: test_sensor[System Monitor IPv4 address eth0 - attributes]
ReadOnlyDict({ ReadOnlyDict({

View file

@ -4,14 +4,11 @@ import socket
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from psutil._common import shwtemp, snetio, snicaddr from psutil._common import sdiskusage, shwtemp, snetio, snicaddr
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.systemmonitor.sensor import ( from homeassistant.components.systemmonitor.sensor import get_cpu_icon
_read_cpu_temperature,
get_cpu_icon,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -218,11 +215,11 @@ async def test_sensor_process_fails(
async def test_sensor_network_sensors( async def test_sensor_network_sensors(
freezer: FrozenDateTimeFactory,
hass: HomeAssistant, hass: HomeAssistant,
entity_registry_enabled_by_default: None, entity_registry_enabled_by_default: None,
mock_added_config_entry: ConfigEntry, mock_added_config_entry: ConfigEntry,
mock_psutil: Mock, mock_psutil: Mock,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test process not exist failure.""" """Test process not exist failure."""
network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1")
@ -306,41 +303,129 @@ async def test_missing_cpu_temperature(
mock_psutil.sensors_temperatures.return_value = { mock_psutil.sensors_temperatures.return_value = {
"not_exist": [shwtemp("not_exist", 50.0, 60.0, 70.0)] "not_exist": [shwtemp("not_exist", 50.0, 60.0, 70.0)]
} }
mock_util.sensors_temperatures.return_value = {
"not_exist": [shwtemp("not_exist", 50.0, 60.0, 70.0)]
}
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert "Cannot read CPU / processor temperature information" in caplog.text # assert "Cannot read CPU / processor temperature information" in caplog.text
temp_sensor = hass.states.get("sensor.system_monitor_processor_temperature") temp_sensor = hass.states.get("sensor.system_monitor_processor_temperature")
assert temp_sensor is None assert temp_sensor is None
async def test_processor_temperature() -> None: async def test_processor_temperature(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_util: Mock,
mock_psutil: Mock,
mock_os: Mock,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the disk failures.""" """Test the disk failures."""
with patch("sys.platform", "linux"), patch( with patch("sys.platform", "linux"):
"homeassistant.components.systemmonitor.sensor.psutil"
) as mock_psutil:
mock_psutil.sensors_temperatures.return_value = { mock_psutil.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)] "cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
} }
temperature = _read_cpu_temperature() mock_psutil.sensors_temperatures.side_effect = None
assert temperature == 50.0 mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
temp_entity = hass.states.get("sensor.system_monitor_processor_temperature")
assert temp_entity.state == "50.0"
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
with patch("sys.platform", "nt"), patch( with patch("sys.platform", "nt"):
"homeassistant.components.systemmonitor.sensor.psutil", mock_psutil.sensors_temperatures.return_value = None
) as mock_psutil:
mock_psutil.sensors_temperatures.side_effect = AttributeError( mock_psutil.sensors_temperatures.side_effect = AttributeError(
"sensors_temperatures not exist" "sensors_temperatures not exist"
) )
temperature = _read_cpu_temperature() mock_config_entry.add_to_hass(hass)
assert temperature is None assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
temp_entity = hass.states.get("sensor.system_monitor_processor_temperature")
assert temp_entity.state == STATE_UNAVAILABLE
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
with patch("sys.platform", "darwin"), patch( with patch("sys.platform", "darwin"):
"homeassistant.components.systemmonitor.sensor.psutil"
) as mock_psutil:
mock_psutil.sensors_temperatures.return_value = { mock_psutil.sensors_temperatures.return_value = {
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)] "cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
} }
temperature = _read_cpu_temperature() mock_psutil.sensors_temperatures.side_effect = None
assert temperature == 50.0 mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
temp_entity = hass.states.get("sensor.system_monitor_processor_temperature")
assert temp_entity.state == "50.0"
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
async def test_exception_handling_disk_sensor(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
mock_psutil: Mock,
mock_added_config_entry: ConfigEntry,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the sensor."""
disk_sensor = hass.states.get("sensor.system_monitor_disk_free")
assert disk_sensor is not None
assert disk_sensor.state == "200.0" # GiB
mock_psutil.disk_usage.return_value = None
mock_psutil.disk_usage.side_effect = OSError("Could not update /")
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
"Error fetching System Monitor Disk / coordinator data: OS error for /"
in caplog.text
)
disk_sensor = hass.states.get("sensor.system_monitor_disk_free")
assert disk_sensor is not None
assert disk_sensor.state == STATE_UNAVAILABLE
mock_psutil.disk_usage.return_value = None
mock_psutil.disk_usage.side_effect = PermissionError("No access to /")
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
"Error fetching System Monitor Disk / coordinator data: OS error for /"
in caplog.text
)
disk_sensor = hass.states.get("sensor.system_monitor_disk_free")
assert disk_sensor is not None
assert disk_sensor.state == STATE_UNAVAILABLE
mock_psutil.disk_usage.return_value = sdiskusage(
500 * 1024**3, 350 * 1024**3, 150 * 1024**3, 70.0
)
mock_psutil.disk_usage.side_effect = None
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
disk_sensor = hass.states.get("sensor.system_monitor_disk_free")
assert disk_sensor is not None
assert disk_sensor.state == "150.0"
assert disk_sensor.attributes["unit_of_measurement"] == "GiB"
disk_sensor = hass.states.get("sensor.system_monitor_disk_usage")
assert disk_sensor is not None
assert disk_sensor.state == "70.0"
assert disk_sensor.attributes["unit_of_measurement"] == "%"