diff --git a/homeassistant/components/systemmonitor/const.py b/homeassistant/components/systemmonitor/const.py index 798cb82f8ef..1f254ca92d6 100644 --- a/homeassistant/components/systemmonitor/const.py +++ b/homeassistant/components/systemmonitor/const.py @@ -1,6 +1,7 @@ """Constants for System Monitor.""" DOMAIN = "systemmonitor" +DOMAIN_COORDINATORS = "systemmonitor_coordinators" CONF_INDEX = "index" CONF_PROCESS = "process" diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index 7b72d334d0d..6f93b9ddce8 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -14,7 +14,10 @@ import psutil_home_assistant as ha_psutil from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + TimestampDataUpdateCoordinator, + UpdateFailed, +) from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -50,7 +53,7 @@ dataT = TypeVar( ) -class MonitorCoordinator(DataUpdateCoordinator[dataT]): +class MonitorCoordinator(TimestampDataUpdateCoordinator[dataT]): """A System monitor Base Data Update Coordinator.""" def __init__( diff --git a/homeassistant/components/systemmonitor/diagnostics.py b/homeassistant/components/systemmonitor/diagnostics.py new file mode 100644 index 00000000000..d48097e936c --- /dev/null +++ b/homeassistant/components/systemmonitor/diagnostics.py @@ -0,0 +1,30 @@ +"""Diagnostics support for Sensibo.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN_COORDINATORS +from .coordinator import MonitorCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for Sensibo config entry.""" + coordinators: dict[str, MonitorCoordinator] = hass.data[DOMAIN_COORDINATORS] + + diag_data = {} + for _type, coordinator in coordinators.items(): + diag_data[_type] = { + "last_update_success": coordinator.last_update_success, + "last_update": str(coordinator.last_update_success_time), + "data": str(coordinator.data), + } + + return { + "entry": entry.as_dict(), + "coordinators": diag_data, + } diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index d099e787719..2688e27c0ac 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -46,7 +46,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import slugify -from .const import CONF_PROCESS, DOMAIN, NET_IO_TYPES +from .const import CONF_PROCESS, DOMAIN, DOMAIN_COORDINATORS, NET_IO_TYPES from .coordinator import ( MonitorCoordinator, SystemMonitorBootTimeCoordinator, @@ -747,18 +747,22 @@ async def async_setup_entry( # noqa: C901 ) ) + hass.data[DOMAIN_COORDINATORS] = {} # No gathering to avoid swamping the executor - for coordinator in disk_coordinators.values(): + for argument, coordinator in disk_coordinators.items(): + hass.data[DOMAIN_COORDINATORS][f"disk_{argument}"] = coordinator + hass.data[DOMAIN_COORDINATORS]["boot_time"] = boot_time_coordinator + hass.data[DOMAIN_COORDINATORS]["cpu_temp"] = cpu_temp_coordinator + hass.data[DOMAIN_COORDINATORS]["memory"] = memory_coordinator + hass.data[DOMAIN_COORDINATORS]["net_addr"] = net_addr_coordinator + hass.data[DOMAIN_COORDINATORS]["net_io"] = net_io_coordinator + hass.data[DOMAIN_COORDINATORS]["process"] = process_coordinator + hass.data[DOMAIN_COORDINATORS]["processor"] = processor_coordinator + hass.data[DOMAIN_COORDINATORS]["swap"] = swap_coordinator + hass.data[DOMAIN_COORDINATORS]["system_load"] = system_load_coordinator + + for coordinator in hass.data[DOMAIN_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) diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index fa3e8bedb56..c48678dcb5f 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -31,6 +31,7 @@ class MockProcess(Process): super().__init__(1) self._name = name self._ex = ex + self._create_time = 1708700400 def name(self): """Return a name.""" @@ -163,6 +164,7 @@ def mock_psutil(mock_process: list[MockProcess]) -> Generator: sdiskpart("test3", "/incorrect", "", "", 1, 1), sdiskpart("proc", "/proc/run", "proc", "", 1, 1), ] + mock_psutil.boot_time.return_value = 1708786800.0 mock_psutil.NoSuchProcess = NoSuchProcess yield mock_psutil diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..0acb2362134 --- /dev/null +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -0,0 +1,83 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'coordinators': dict({ + 'boot_time': dict({ + 'data': '2024-02-24 15:00:00+00:00', + 'last_update_success': True, + }), + 'cpu_temp': dict({ + 'data': "{'cpu0-thermal': [shwtemp(label='cpu0-thermal', current=50.0, high=60.0, critical=70.0)]}", + 'last_update_success': True, + }), + 'disk_/': dict({ + 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + 'last_update_success': True, + }), + 'disk_/home/notexist/': dict({ + 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + 'last_update_success': True, + }), + 'disk_/media/share': dict({ + 'data': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', + 'last_update_success': True, + }), + 'memory': dict({ + 'data': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', + 'last_update_success': True, + }), + 'net_addr': dict({ + 'data': "{'eth0': [snicaddr(family=, address='192.168.1.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)], 'eth1': [snicaddr(family=, address='192.168.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)], 'vethxyzxyz': [snicaddr(family=, address='172.16.10.1', netmask='255.255.255.0', broadcast='255.255.255.255', ptp=None)]}", + 'last_update_success': True, + }), + 'net_io': dict({ + 'data': "{'eth0': snetio(bytes_sent=104857600, bytes_recv=104857600, packets_sent=50, packets_recv=50, errin=0, errout=0, dropin=0, dropout=0), 'eth1': snetio(bytes_sent=209715200, bytes_recv=209715200, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0), 'vethxyzxyz': snetio(bytes_sent=314572800, bytes_recv=314572800, packets_sent=150, packets_recv=150, errin=0, errout=0, dropin=0, dropout=0)}", + 'last_update_success': True, + }), + 'process': dict({ + 'data': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", + 'last_update_success': True, + }), + 'processor': dict({ + 'data': '10.0', + 'last_update_success': True, + }), + 'swap': dict({ + 'data': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', + 'last_update_success': True, + }), + 'system_load': dict({ + 'data': '(1, 2, 3)', + 'last_update_success': True, + }), + }), + 'entry': dict({ + 'data': dict({ + }), + 'disabled_by': None, + 'domain': 'systemmonitor', + 'minor_version': 2, + 'options': dict({ + 'binary_sensor': dict({ + 'process': list([ + 'python3', + 'pip', + ]), + }), + 'resources': list([ + 'disk_use_percent_/', + 'disk_use_percent_/home/notexist/', + 'memory_free_', + 'network_out_eth0', + 'process_python3', + ]), + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'System Monitor', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 3708ca1e53a..952aaaa7ec2 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -123,7 +123,7 @@ }) # --- # name: test_sensor[System Monitor Last boot - state] - '2023-12-30T21:55:38+00:00' + '2024-02-24T15:00:00+00:00' # --- # name: test_sensor[System Monitor Load (15m) - attributes] ReadOnlyDict({ diff --git a/tests/components/systemmonitor/test_diagnostics.py b/tests/components/systemmonitor/test_diagnostics.py new file mode 100644 index 00000000000..b50f1aa16b2 --- /dev/null +++ b/tests/components/systemmonitor/test_diagnostics.py @@ -0,0 +1,25 @@ +"""Tests for the diagnostics data provided by the System Monitor integration.""" +from unittest.mock import Mock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_psutil: Mock, + mock_os: Mock, + mock_added_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, mock_added_config_entry + ) == snapshot(exclude=props("last_update", "entry_id"))