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_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",
|
||||||
]
|
]
|
||||||
|
|
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."""
|
"""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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"] == "%"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue