Create update coordinator for Systemmonitor (#106693)
This commit is contained in:
parent
d5c1049bfe
commit
d4f9ad9dd3
7 changed files with 678 additions and 398 deletions
|
@ -5,13 +5,37 @@ DOMAIN = "systemmonitor"
|
|||
CONF_INDEX = "index"
|
||||
CONF_PROCESS = "process"
|
||||
|
||||
NETWORK_TYPES = [
|
||||
NET_IO_TYPES = [
|
||||
"network_in",
|
||||
"network_out",
|
||||
"throughput_network_in",
|
||||
"throughput_network_out",
|
||||
"packets_in",
|
||||
"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",
|
||||
]
|
||||
|
|
166
homeassistant/components/systemmonitor/coordinator.py
Normal file
166
homeassistant/components/systemmonitor/coordinator.py
Normal 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
|
|
@ -1,17 +1,18 @@
|
|||
"""Support for monitoring the local system."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cache, lru_cache
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from typing import Any, Literal
|
||||
import time
|
||||
from typing import Any, Generic, Literal
|
||||
|
||||
import psutil
|
||||
from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
|
@ -26,7 +27,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
|||
from homeassistant.const import (
|
||||
CONF_RESOURCES,
|
||||
CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
PERCENTAGE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
|
@ -35,22 +35,31 @@ from homeassistant.const import (
|
|||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
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,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
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
|
||||
from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES
|
||||
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__)
|
||||
|
||||
|
@ -74,16 +83,92 @@ def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]:
|
|||
return "mdi:cpu-32-bit"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SysMonitorSensorEntityDescription(SensorEntityDescription):
|
||||
"""Description for System Monitor sensor entities."""
|
||||
def get_processor_temperature(
|
||||
entity: SystemMonitorSensor[dict[str, list[shwtemp]]],
|
||||
) -> 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
|
||||
placeholder: str | None = None
|
||||
|
||||
|
||||
SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
||||
"disk_free": SysMonitorSensorEntityDescription(
|
||||
SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription[Any]] = {
|
||||
"disk_free": SysMonitorSensorEntityDescription[sdiskusage](
|
||||
key="disk_free",
|
||||
translation_key="disk_free",
|
||||
placeholder="mount_point",
|
||||
|
@ -91,8 +176,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:harddisk",
|
||||
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",
|
||||
translation_key="disk_use",
|
||||
placeholder="mount_point",
|
||||
|
@ -100,76 +186,91 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:harddisk",
|
||||
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",
|
||||
translation_key="disk_use_percent",
|
||||
placeholder="mount_point",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:harddisk",
|
||||
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",
|
||||
translation_key="ipv4_address",
|
||||
placeholder="ip_address",
|
||||
icon="mdi:ip-network",
|
||||
mandatory_arg=True,
|
||||
value_fn=get_ip_address,
|
||||
),
|
||||
"ipv6_address": SysMonitorSensorEntityDescription(
|
||||
"ipv6_address": SysMonitorSensorEntityDescription[dict[str, list[snicaddr]]](
|
||||
key="ipv6_address",
|
||||
translation_key="ipv6_address",
|
||||
placeholder="ip_address",
|
||||
icon="mdi:ip-network",
|
||||
mandatory_arg=True,
|
||||
value_fn=get_ip_address,
|
||||
),
|
||||
"last_boot": SysMonitorSensorEntityDescription(
|
||||
"last_boot": SysMonitorSensorEntityDescription[datetime](
|
||||
key="last_boot",
|
||||
translation_key="last_boot",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda entity: entity.coordinator.data,
|
||||
),
|
||||
"load_15m": SysMonitorSensorEntityDescription(
|
||||
"load_15m": SysMonitorSensorEntityDescription[tuple[float, float, float]](
|
||||
key="load_15m",
|
||||
translation_key="load_15m",
|
||||
icon=get_cpu_icon(),
|
||||
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",
|
||||
translation_key="load_1m",
|
||||
icon=get_cpu_icon(),
|
||||
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",
|
||||
translation_key="load_5m",
|
||||
icon=get_cpu_icon(),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda entity: round(entity.coordinator.data[1], 2),
|
||||
),
|
||||
"memory_free": SysMonitorSensorEntityDescription(
|
||||
"memory_free": SysMonitorSensorEntityDescription[VirtualMemory](
|
||||
key="memory_free",
|
||||
translation_key="memory_free",
|
||||
native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:memory",
|
||||
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",
|
||||
translation_key="memory_use",
|
||||
native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:memory",
|
||||
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",
|
||||
translation_key="memory_use_percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:memory",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda entity: entity.coordinator.data.percent,
|
||||
),
|
||||
"network_in": SysMonitorSensorEntityDescription(
|
||||
"network_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="network_in",
|
||||
translation_key="network_in",
|
||||
placeholder="interface",
|
||||
|
@ -178,8 +279,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
icon="mdi:server-network",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_network,
|
||||
),
|
||||
"network_out": SysMonitorSensorEntityDescription(
|
||||
"network_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="network_out",
|
||||
translation_key="network_out",
|
||||
placeholder="interface",
|
||||
|
@ -188,24 +290,27 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
icon="mdi:server-network",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_network,
|
||||
),
|
||||
"packets_in": SysMonitorSensorEntityDescription(
|
||||
"packets_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="packets_in",
|
||||
translation_key="packets_in",
|
||||
placeholder="interface",
|
||||
icon="mdi:server-network",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_packets,
|
||||
),
|
||||
"packets_out": SysMonitorSensorEntityDescription(
|
||||
"packets_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="packets_out",
|
||||
translation_key="packets_out",
|
||||
placeholder="interface",
|
||||
icon="mdi:server-network",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_packets,
|
||||
),
|
||||
"throughput_network_in": SysMonitorSensorEntityDescription(
|
||||
"throughput_network_in": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="throughput_network_in",
|
||||
translation_key="throughput_network_in",
|
||||
placeholder="interface",
|
||||
|
@ -213,8 +318,9 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_throughput,
|
||||
),
|
||||
"throughput_network_out": SysMonitorSensorEntityDescription(
|
||||
"throughput_network_out": SysMonitorSensorEntityDescription[dict[str, snetio]](
|
||||
key="throughput_network_out",
|
||||
translation_key="throughput_network_out",
|
||||
placeholder="interface",
|
||||
|
@ -222,50 +328,59 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
|
|||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
mandatory_arg=True,
|
||||
value_fn=get_throughput,
|
||||
),
|
||||
"process": SysMonitorSensorEntityDescription(
|
||||
"process": SysMonitorSensorEntityDescription[list[psutil.Process]](
|
||||
key="process",
|
||||
translation_key="process",
|
||||
placeholder="process",
|
||||
icon=get_cpu_icon(),
|
||||
mandatory_arg=True,
|
||||
value_fn=get_process,
|
||||
),
|
||||
"processor_use": SysMonitorSensorEntityDescription(
|
||||
"processor_use": SysMonitorSensorEntityDescription[float](
|
||||
key="processor_use",
|
||||
translation_key="processor_use",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon=get_cpu_icon(),
|
||||
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",
|
||||
translation_key="processor_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_processor_temperature,
|
||||
),
|
||||
"swap_free": SysMonitorSensorEntityDescription(
|
||||
"swap_free": SysMonitorSensorEntityDescription[sswap](
|
||||
key="swap_free",
|
||||
translation_key="swap_free",
|
||||
native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:harddisk",
|
||||
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",
|
||||
translation_key="swap_use",
|
||||
native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
icon="mdi:harddisk",
|
||||
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",
|
||||
translation_key="swap_use_percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
icon="mdi:harddisk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda entity: entity.coordinator.data.percent,
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -320,46 +435,8 @@ IO_COUNTER = {
|
|||
"throughput_network_out": 0,
|
||||
"throughput_network_in": 1,
|
||||
}
|
||||
|
||||
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(
|
||||
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
|
||||
) -> None:
|
||||
"""Set up System Montor sensors based on a config entry."""
|
||||
entities = []
|
||||
sensor_registry: dict[tuple[str, str], SensorData] = {}
|
||||
entities: list[SystemMonitorSensor] = []
|
||||
legacy_resources: set[str] = set(entry.options.get("resources", []))
|
||||
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)
|
||||
cpu_temperature = await hass.async_add_executor_job(_read_cpu_temperature)
|
||||
|
||||
def get_arguments() -> dict[str, Any]:
|
||||
"""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)
|
||||
|
||||
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
|
||||
)
|
||||
for argument in startup_arguments["disk_arguments"]:
|
||||
is_enabled = check_legacy_resource(
|
||||
f"{_type}_{argument}", legacy_resources
|
||||
)
|
||||
loaded_resources.add(slugify(f"{_type}_{argument}"))
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
disk_coordinators[argument],
|
||||
sensor_description,
|
||||
entry.entry_id,
|
||||
argument,
|
||||
|
@ -434,18 +547,15 @@ async def async_setup_entry(
|
|||
)
|
||||
continue
|
||||
|
||||
if _type in NETWORK_TYPES:
|
||||
for argument in network_arguments:
|
||||
sensor_registry[(_type, argument)] = SensorData(
|
||||
argument, None, None, None, None
|
||||
)
|
||||
if _type.startswith("ipv"):
|
||||
for argument in startup_arguments["network_arguments"]:
|
||||
is_enabled = check_legacy_resource(
|
||||
f"{_type}_{argument}", legacy_resources
|
||||
)
|
||||
loaded_resources.add(slugify(f"{_type}_{argument}"))
|
||||
loaded_resources.add(f"{_type}_{argument}")
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
net_addr_coordinator,
|
||||
sensor_description,
|
||||
entry.entry_id,
|
||||
argument,
|
||||
|
@ -454,22 +564,74 @@ async def async_setup_entry(
|
|||
)
|
||||
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 cpu_temperature is None:
|
||||
_LOGGER.warning("Cannot read CPU / processor temperature information")
|
||||
if _type == "last_boot":
|
||||
argument = ""
|
||||
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
|
||||
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
|
||||
|
||||
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, []):
|
||||
sensor_registry[(_type, argument)] = SensorData(
|
||||
argument, None, None, None, None
|
||||
)
|
||||
loaded_resources.add(slugify(f"{_type}_{argument}"))
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
process_coordinator,
|
||||
sensor_description,
|
||||
entry.entry_id,
|
||||
argument,
|
||||
|
@ -478,15 +640,49 @@ async def async_setup_entry(
|
|||
)
|
||||
continue
|
||||
|
||||
sensor_registry[(_type, "")] = SensorData("", None, None, None, None)
|
||||
is_enabled = check_legacy_resource(f"{_type}_", legacy_resources)
|
||||
loaded_resources.add(f"{_type}_")
|
||||
if _type == "processor_use":
|
||||
argument = ""
|
||||
is_enabled = check_legacy_resource(f"{_type}_{argument}", legacy_resources)
|
||||
loaded_resources.add(f"{_type}_{argument}")
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
processor_coordinator,
|
||||
sensor_description,
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
@ -506,12 +702,13 @@ async def async_setup_entry(
|
|||
_type = resource[:split_index]
|
||||
argument = resource[split_index + 1 :]
|
||||
_LOGGER.debug("Loading legacy %s with argument %s", _type, argument)
|
||||
sensor_registry[(_type, argument)] = SensorData(
|
||||
argument, None, None, None, None
|
||||
if not disk_coordinators.get(argument):
|
||||
disk_coordinators[argument] = SystemMonitorDiskCoordinator(
|
||||
hass, f"Disk {argument} coordinator", argument
|
||||
)
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
disk_coordinators[argument],
|
||||
SENSOR_TYPES[_type],
|
||||
entry.entry_id,
|
||||
argument,
|
||||
|
@ -519,94 +716,45 @@ async def async_setup_entry(
|
|||
)
|
||||
)
|
||||
|
||||
scan_interval = DEFAULT_SCAN_INTERVAL
|
||||
await async_setup_sensor_registry_updates(hass, sensor_registry, scan_interval)
|
||||
# No gathering to avoid swamping the executor
|
||||
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 def async_setup_sensor_registry_updates(
|
||||
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):
|
||||
class SystemMonitorSensor(CoordinatorEntity[MonitorCoordinator[dataT]], SensorEntity):
|
||||
"""Implementation of a system monitor sensor."""
|
||||
|
||||
should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: SysMonitorSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sensor_registry: dict[tuple[str, str], SensorData],
|
||||
coordinator: MonitorCoordinator,
|
||||
sensor_description: SysMonitorSensorEntityDescription,
|
||||
entry_id: str,
|
||||
argument: str = "",
|
||||
argument: str,
|
||||
legacy_enabled: bool = False,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = sensor_description
|
||||
if self.entity_description.placeholder:
|
||||
self._attr_translation_placeholders = {
|
||||
self.entity_description.placeholder: 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_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
|
@ -614,177 +762,11 @@ class SystemMonitorSensor(SensorEntity):
|
|||
manufacturer="System Monitor",
|
||||
name="System Monitor",
|
||||
)
|
||||
self.argument = argument
|
||||
self.value: int | None = None
|
||||
self.update_time: float | None = None
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | datetime | None:
|
||||
"""Return the state of the device."""
|
||||
return self.data.state
|
||||
|
||||
@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
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(self)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""Utils for System Monitor."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import psutil
|
||||
from psutil._common import shwtemp
|
||||
|
||||
from .const import CPU_SENSOR_PREFIXES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -61,3 +63,22 @@ def get_all_running_processes() -> set[str]:
|
|||
processes.add(proc.name())
|
||||
_LOGGER.debug("Running processes: %s", ", ".join(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
|
||||
|
|
|
@ -115,11 +115,11 @@ def mock_process() -> list[MockProcess]:
|
|||
def mock_psutil(mock_process: list[MockProcess]) -> Mock:
|
||||
"""Mock psutil."""
|
||||
with patch(
|
||||
"homeassistant.components.systemmonitor.sensor.psutil",
|
||||
"homeassistant.components.systemmonitor.coordinator.psutil",
|
||||
autospec=True,
|
||||
) as mock_psutil:
|
||||
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(
|
||||
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
|
||||
def mock_os() -> Mock:
|
||||
"""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"
|
||||
) as mock_os_util:
|
||||
mock_os_util.name = "nt"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
})
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk free / - state]
|
||||
'0.2'
|
||||
'200.0'
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk free /media/share - attributes]
|
||||
ReadOnlyDict({
|
||||
|
@ -21,7 +21,7 @@
|
|||
})
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk free /media/share - state]
|
||||
'0.2'
|
||||
'200.0'
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk usage / - attributes]
|
||||
ReadOnlyDict({
|
||||
|
@ -66,7 +66,7 @@
|
|||
})
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk use / - state]
|
||||
'0.3'
|
||||
'300.0'
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk use /media/share - attributes]
|
||||
ReadOnlyDict({
|
||||
|
@ -78,7 +78,7 @@
|
|||
})
|
||||
# ---
|
||||
# name: test_sensor[System Monitor Disk use /media/share - state]
|
||||
'0.3'
|
||||
'300.0'
|
||||
# ---
|
||||
# name: test_sensor[System Monitor IPv4 address eth0 - attributes]
|
||||
ReadOnlyDict({
|
||||
|
|
|
@ -4,14 +4,11 @@ import socket
|
|||
from unittest.mock import Mock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from psutil._common import shwtemp, snetio, snicaddr
|
||||
from psutil._common import sdiskusage, shwtemp, snetio, snicaddr
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.systemmonitor.sensor import (
|
||||
_read_cpu_temperature,
|
||||
get_cpu_icon,
|
||||
)
|
||||
from homeassistant.components.systemmonitor.sensor import get_cpu_icon
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -218,11 +215,11 @@ async def test_sensor_process_fails(
|
|||
|
||||
|
||||
async def test_sensor_network_sensors(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass: HomeAssistant,
|
||||
entity_registry_enabled_by_default: None,
|
||||
mock_added_config_entry: ConfigEntry,
|
||||
mock_psutil: Mock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test process not exist failure."""
|
||||
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 = {
|
||||
"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)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
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")
|
||||
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."""
|
||||
|
||||
with patch("sys.platform", "linux"), patch(
|
||||
"homeassistant.components.systemmonitor.sensor.psutil"
|
||||
) as mock_psutil:
|
||||
with patch("sys.platform", "linux"):
|
||||
mock_psutil.sensors_temperatures.return_value = {
|
||||
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
|
||||
}
|
||||
temperature = _read_cpu_temperature()
|
||||
assert temperature == 50.0
|
||||
mock_psutil.sensors_temperatures.side_effect = None
|
||||
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(
|
||||
"homeassistant.components.systemmonitor.sensor.psutil",
|
||||
) as mock_psutil:
|
||||
with patch("sys.platform", "nt"):
|
||||
mock_psutil.sensors_temperatures.return_value = None
|
||||
mock_psutil.sensors_temperatures.side_effect = AttributeError(
|
||||
"sensors_temperatures not exist"
|
||||
)
|
||||
temperature = _read_cpu_temperature()
|
||||
assert temperature is None
|
||||
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 == 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(
|
||||
"homeassistant.components.systemmonitor.sensor.psutil"
|
||||
) as mock_psutil:
|
||||
with patch("sys.platform", "darwin"):
|
||||
mock_psutil.sensors_temperatures.return_value = {
|
||||
"cpu0-thermal": [shwtemp("cpu0-thermal", 50.0, 60.0, 70.0)]
|
||||
}
|
||||
temperature = _read_cpu_temperature()
|
||||
assert temperature == 50.0
|
||||
mock_psutil.sensors_temperatures.side_effect = None
|
||||
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"] == "%"
|
||||
|
|
Loading…
Add table
Reference in a new issue