From 9681b508ce553ac14caf79e76dec90f52588f72d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 23 Aug 2025 13:40:37 +0000 Subject: [PATCH] Fix --- .../components/systemmonitor/binary_sensor.py | 31 ++++++++-- .../components/systemmonitor/icons.json | 4 +- .../components/systemmonitor/sensor.py | 29 ++++++++- .../components/systemmonitor/strings.json | 4 +- tests/components/systemmonitor/conftest.py | 3 +- .../snapshots/test_binary_sensor.ambr | 9 +++ .../snapshots/test_diagnostics.ambr | 10 +++ .../systemmonitor/snapshots/test_sensor.ambr | 62 ++++++++++++++++--- tests/components/systemmonitor/test_sensor.py | 4 +- 9 files changed, 131 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/systemmonitor/binary_sensor.py b/homeassistant/components/systemmonitor/binary_sensor.py index 3968e94ec03..e77169aacb7 100644 --- a/homeassistant/components/systemmonitor/binary_sensor.py +++ b/homeassistant/components/systemmonitor/binary_sensor.py @@ -72,11 +72,11 @@ def get_process(entity: SystemMonitorSensor) -> bool: class SysMonitorBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes System Monitor binary sensor entities.""" - value_fn: Callable[[SystemMonitorSensor], bool] + value_fn: Callable[[SystemMonitorSensor], bool | None] add_to_update: Callable[[SystemMonitorSensor], tuple[str, str]] -SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( +PROCESS_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( SysMonitorBinarySensorEntityDescription( key="binary_process", translation_key="process", @@ -87,6 +87,17 @@ SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( ), ) +BINARY_SENSOR_TYPES: tuple[SysMonitorBinarySensorEntityDescription, ...] = ( + SysMonitorBinarySensorEntityDescription( + key="battery_plugged", + value_fn=lambda entity: entity.coordinator.data.battery.power_plugged + if entity.coordinator.data.battery + else None, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + add_to_update=lambda entity: ("battery", ""), + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -96,18 +107,30 @@ async def async_setup_entry( """Set up System Monitor binary sensors based on a config entry.""" coordinator = entry.runtime_data.coordinator - async_add_entities( + entities: list[SystemMonitorSensor] = [] + + entities.extend( SystemMonitorSensor( coordinator, sensor_description, entry.entry_id, argument, ) - for sensor_description in SENSOR_TYPES + for sensor_description in PROCESS_TYPES for argument in entry.options.get(BINARY_SENSOR_DOMAIN, {}).get( CONF_PROCESS, [] ) ) + entities.extend( + SystemMonitorSensor( + coordinator, + sensor_description, + entry.entry_id, + "", + ) + for sensor_description in BINARY_SENSOR_TYPES + ) + async_add_entities(entities) class SystemMonitorSensor( diff --git a/homeassistant/components/systemmonitor/icons.json b/homeassistant/components/systemmonitor/icons.json index 9df981f71f8..5f7db25b8a9 100644 --- a/homeassistant/components/systemmonitor/icons.json +++ b/homeassistant/components/systemmonitor/icons.json @@ -1,8 +1,8 @@ { "entity": { "sensor": { - "battery": { - "default": "mdi:battery" + "battery_left": { + "default": "mdi:battery-clock" }, "disk_free": { "default": "mdi:harddisk" diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 88361534646..9097fa81928 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -14,6 +14,8 @@ import sys import time from typing import Any, Literal +from psutil._common import POWER_TIME_UNKNOWN, POWER_TIME_UNLIMITED + from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -28,6 +30,7 @@ from homeassistant.const import ( UnitOfDataRate, UnitOfInformation, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -55,6 +58,8 @@ SENSOR_TYPE_MANDATORY_ARG = 4 SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update" +BATTERY_REMAIN_UNKNOWNS = (POWER_TIME_UNKNOWN, POWER_TIME_UNLIMITED) + @lru_cache def get_cpu_icon() -> Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"]: @@ -119,6 +124,14 @@ def get_ip_address( return None +def battery_seconds_left(entity: SystemMonitorSensor) -> int | None: + """Return remaining battery time in seconds.""" + battery = entity.coordinator.data.battery + if not battery or battery.secsleft in BATTERY_REMAIN_UNKNOWNS: + return None + return battery.secsleft + + @dataclass(frozen=True, kw_only=True) class SysMonitorSensorEntityDescription(SensorEntityDescription): """Describes System Monitor sensor entities.""" @@ -133,7 +146,6 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { "battery": SysMonitorSensorEntityDescription( key="battery", - translation_key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -143,6 +155,17 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { none_is_unavailable=True, add_to_update=lambda entity: ("battery", ""), ), + "battery_left": SysMonitorSensorEntityDescription( + key="battery_left", + translation_key="battery_left", + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + value_fn=battery_seconds_left, + none_is_unavailable=True, + add_to_update=lambda entity: ("battery", ""), + suggested_unit_of_measurement=UnitOfTime.MINUTES, + ), "disk_free": SysMonitorSensorEntityDescription( key="disk_free", translation_key="disk_free", @@ -190,7 +213,7 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { "fan_rpm": SysMonitorSensorEntityDescription( key="fan_rpm", translation_key="fan_rpm", - placeholder="name", + placeholder="fan_name", native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda entity: entity.coordinator.data.fan_rpm[entity.argument], @@ -590,7 +613,7 @@ async def async_setup_entry( # noqa: C901 ) ) - if _type == "battery": + if _type.startswith("battery"): argument = "" loaded_resources.add(slugify(f"{_type}_{argument}")) entities.append( diff --git a/homeassistant/components/systemmonitor/strings.json b/homeassistant/components/systemmonitor/strings.json index e026bb90b1e..5b16741c2e5 100644 --- a/homeassistant/components/systemmonitor/strings.json +++ b/homeassistant/components/systemmonitor/strings.json @@ -29,7 +29,7 @@ } }, "sensor": { - "battery": { + "battery_left": { "name": "Battery" }, "disk_free": { @@ -42,7 +42,7 @@ "name": "Disk usage {mount_point}" }, "fan_rpm": { - "name": "{name} fan RPM" + "name": "{fan_name} fan RPM" }, "ipv4_address": { "name": "IPv4 address {ip_address}" diff --git a/tests/components/systemmonitor/conftest.py b/tests/components/systemmonitor/conftest.py index 9d0bbf37760..b5cacea4d25 100644 --- a/tests/components/systemmonitor/conftest.py +++ b/tests/components/systemmonitor/conftest.py @@ -193,8 +193,7 @@ def mock_psutil(mock_process: list[MockProcess]) -> Generator: mock_psutil.NoSuchProcess = NoSuchProcess # mock_psutil.sensors_fans = Mock() mock_psutil.sensors_fans.return_value = { - "fan1": [sfan("fan1", 1200)], - "fan2": [sfan("fan2", 1300)], + "asus": [sfan("cpu-fan", 1200), sfan("another-fan", 1300)], } mock_psutil.sensors_battery.return_value = sbattery( percent=93, secsleft=16628, power_plugged=False diff --git a/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr index 0c04cfcfa06..2c914c53afb 100644 --- a/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_binary_sensor.ambr @@ -1,4 +1,13 @@ # serializer version: 1 +# name: test_binary_sensor[System Monitor Charging - attributes] + ReadOnlyDict({ + 'device_class': 'battery_charging', + 'friendly_name': 'System Monitor Charging', + }) +# --- +# name: test_binary_sensor[System Monitor Charging - state] + 'off' +# --- # name: test_binary_sensor[System Monitor Process pip - attributes] ReadOnlyDict({ 'device_class': 'running', diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index afa508cc004..9f695157327 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -8,6 +8,7 @@ '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)]", }), + 'battery': 'sbattery(percent=93, secsleft=16628, power_plugged=False)', 'boot_time': '2024-02-24 15:00:00+00:00', 'cpu_percent': '10.0', 'disk_usage': dict({ @@ -15,6 +16,10 @@ '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', }), + 'fan_rpm': dict({ + 'another-fan': '1300', + 'cpu-fan': '1200', + }), 'io_counters': dict({ '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)', @@ -69,6 +74,7 @@ 'coordinators': dict({ 'data': dict({ 'addresses': None, + 'battery': 'sbattery(percent=93, secsleft=16628, power_plugged=False)', 'boot_time': '2024-02-24 15:00:00+00:00', 'cpu_percent': '10.0', 'disk_usage': dict({ @@ -76,6 +82,10 @@ '/home/notexist/': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', '/media/share': 'sdiskusage(total=536870912000, used=322122547200, free=214748364800, percent=60.0)', }), + 'fan_rpm': dict({ + 'another-fan': '1300', + 'cpu-fan': '1200', + }), 'io_counters': None, 'load': '(1, 2, 3)', 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', diff --git a/tests/components/systemmonitor/snapshots/test_sensor.ambr b/tests/components/systemmonitor/snapshots/test_sensor.ambr index 8108e4777c8..ee942ef3296 100644 --- a/tests/components/systemmonitor/snapshots/test_sensor.ambr +++ b/tests/components/systemmonitor/snapshots/test_sensor.ambr @@ -1,4 +1,15 @@ # serializer version: 1 +# name: test_sensor[System Monitor Battery - attributes] + ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'System Monitor Battery', + 'state_class': , + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[System Monitor Battery - state] + '93' +# --- # name: test_sensor[System Monitor Disk free / - attributes] ReadOnlyDict({ 'device_class': 'data_size', @@ -73,6 +84,17 @@ # name: test_sensor[System Monitor Disk use /media/share - state] '300.0' # --- +# name: test_sensor[System Monitor Duration - attributes] + ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'System Monitor Duration', + 'state_class': , + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[System Monitor Duration - state] + '16628' +# --- # name: test_sensor[System Monitor IPv4 address eth0 - attributes] ReadOnlyDict({ 'friendly_name': 'System Monitor IPv4 address eth0', @@ -114,16 +136,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': , - }) -# --- -# 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 +146,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': , + }) +# --- +# 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)', @@ -354,3 +376,23 @@ # name: test_sensor[System Monitor Swap use - state] '60.0' # --- +# name: test_sensor[System Monitor another-fan fan RPM - attributes] + ReadOnlyDict({ + 'friendly_name': 'System Monitor another-fan fan RPM', + 'state_class': , + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensor[System Monitor another-fan fan RPM - state] + '1300' +# --- +# name: test_sensor[System Monitor cpu-fan fan RPM - attributes] + ReadOnlyDict({ + 'friendly_name': 'System Monitor cpu-fan fan RPM', + 'state_class': , + 'unit_of_measurement': 'rpm', + }) +# --- +# name: test_sensor[System Monitor cpu-fan fan RPM - state] + '1200' +# --- diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index a5f5e7623e9..3877555d1ae 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -453,7 +453,7 @@ async def test_remove_obsolete_entities( mock_added_config_entry.entry_id ) ) - == 37 + == 42 ) entity_registry.async_update_entity( @@ -494,7 +494,7 @@ async def test_remove_obsolete_entities( mock_added_config_entry.entry_id ) ) - == 38 + == 43 ) assert (