Add python3 open file descriptor sensor to Systemmonitor

This commit is contained in:
G Johansson
2025-08-14 13:15:27 +00:00
parent e6103fdcf4
commit 8042504339
5 changed files with 115 additions and 13 deletions

View File

@@ -118,6 +118,17 @@ def get_ip_address(
return None
def get_python3_num_fds(
entity: SystemMonitorSensor,
) -> int | None:
"""Return num_fds from python3 process."""
processes = entity.coordinator.data.processes
for proc in processes:
if proc.name() == "python3":
return proc.num_fds()
return None
@dataclass(frozen=True, kw_only=True)
class SysMonitorSensorEntityDescription(SensorEntityDescription):
"""Describes System Monitor sensor entities."""
@@ -369,6 +380,13 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
value_fn=lambda entity: entity.coordinator.data.swap.percent,
add_to_update=lambda entity: ("swap", ""),
),
"python3_num_fds": SysMonitorSensorEntityDescription(
key="python3_num_fds",
translation_key="python3_num_fds",
state_class=SensorStateClass.MEASUREMENT,
value_fn=get_python3_num_fds,
add_to_update=lambda entity: ("processes", ""),
),
}
@@ -566,6 +584,18 @@ async def async_setup_entry(
is_enabled,
)
)
if _type == "python3_num_fds":
argument = ""
loaded_resources.add(slugify(f"{_type}_{argument}"))
entities.append(
SystemMonitorSensor(
coordinator,
sensor_description,
entry.entry_id,
argument,
True, # Enabled by default
)
)
# Ensure legacy imported disk_* resources are loaded if they are not part
# of mount points automatically discovered

View File

@@ -100,6 +100,9 @@
},
"swap_use_percent": {
"name": "Swap usage"
},
"python3_num_fds": {
"name": "Python3 open file descriptors"
}
}
}

View File

@@ -27,12 +27,13 @@ def mock_sys_platform() -> Generator[None]:
class MockProcess(Process):
"""Mock a Process class."""
def __init__(self, name: str, ex: bool = False) -> None:
def __init__(self, name: str, ex: bool = False, num_fds: int = 3) -> None:
"""Initialize the process."""
super().__init__(1)
self._name = name
self._ex = ex
self._create_time = 1708700400
self._num_fds = num_fds
def name(self):
"""Return a name."""
@@ -40,6 +41,10 @@ class MockProcess(Process):
raise NoSuchProcess(1, self._name)
return self._name
def num_fds(self):
"""Return open file descriptors."""
return self._num_fds
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:

View File

@@ -114,16 +114,6 @@
# name: test_sensor[System Monitor Last boot - state]
'2024-02-24T15:00:00+00:00'
# ---
# name: test_sensor[System Monitor Load (15 min) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (15 min)',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Load (15 min) - state]
'3'
# ---
# name: test_sensor[System Monitor Load (1 min) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (1 min)',
@@ -134,6 +124,16 @@
# name: test_sensor[System Monitor Load (1 min) - state]
'1'
# ---
# name: test_sensor[System Monitor Load (15 min) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (15 min)',
'icon': 'mdi:cpu-64-bit',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Load (15 min) - state]
'3'
# ---
# name: test_sensor[System Monitor Load (5 min) - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Load (5 min)',
@@ -322,6 +322,15 @@
# name: test_sensor[System Monitor Processor use - state]
'10'
# ---
# name: test_sensor[System Monitor Python3 open file descriptors - attributes]
ReadOnlyDict({
'friendly_name': 'System Monitor Python3 open file descriptors',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
})
# ---
# name: test_sensor[System Monitor Python3 open file descriptors - state]
'3'
# ---
# name: test_sensor[System Monitor Swap free - attributes]
ReadOnlyDict({
'device_class': 'data_size',

View File

@@ -18,6 +18,8 @@ from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import MockProcess
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -453,7 +455,7 @@ async def test_remove_obsolete_entities(
mock_added_config_entry.entry_id
)
)
== 37
== 38
)
entity_registry.async_update_entity(
@@ -494,7 +496,7 @@ async def test_remove_obsolete_entities(
mock_added_config_entry.entry_id
)
)
== 38
== 39
)
assert (
@@ -544,3 +546,56 @@ async def test_no_duplicate_disk_entities(
assert disk_sensor.state == "60.0"
assert "Platform systemmonitor does not generate unique IDs." not in caplog.text
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_python3_num_fds(
hass: HomeAssistant,
mock_psutil: Mock,
mock_os: Mock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test python3 open file descriptors sensor."""
mock_config_entry = MockConfigEntry(
title="System Monitor",
domain=DOMAIN,
data={},
options={
"resources": [
"disk_use_percent_/",
"disk_use_percent_/home/notexist/",
"memory_free_",
"network_out_eth0",
"process_python3",
],
},
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
num_fds_sensor = hass.states.get(
"sensor.system_monitor_python3_open_file_descriptors"
)
assert num_fds_sensor is not None
assert num_fds_sensor.state == "3"
assert num_fds_sensor.attributes == {
"state_class": "measurement",
"friendly_name": "System Monitor Python3 open file descriptors",
}
_process = MockProcess("python3", num_fds=5)
assert _process.num_fds() == 5
mock_psutil.process_iter.return_value = [_process]
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
num_fds_sensor = hass.states.get(
"sensor.system_monitor_python3_open_file_descriptors"
)
assert num_fds_sensor is not None
assert num_fds_sensor.state == "5"