Create update coordinator for Systemmonitor (#106693)
parent
d5c1049bfe
commit
d4f9ad9dd3
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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…
Reference in New Issue