55from collections .abc import Callable
66import contextlib
77from dataclasses import dataclass
8- from datetime import datetime
8+ from datetime import datetime , timedelta
99from functools import lru_cache
1010import ipaddress
1111import logging
1414import time
1515from typing import Any , Literal
1616
17+ from psutil ._common import POWER_TIME_UNKNOWN , POWER_TIME_UNLIMITED
18+
1719from homeassistant .components .sensor import (
1820 DOMAIN as SENSOR_DOMAIN ,
1921 SensorDeviceClass ,
2325)
2426from homeassistant .const import (
2527 PERCENTAGE ,
28+ REVOLUTIONS_PER_MINUTE ,
2629 EntityCategory ,
2730 UnitOfDataRate ,
2831 UnitOfInformation ,
3437from homeassistant .helpers .entity_platform import AddConfigEntryEntitiesCallback
3538from homeassistant .helpers .typing import StateType
3639from homeassistant .helpers .update_coordinator import CoordinatorEntity
37- from homeassistant .util import slugify
40+ from homeassistant .util import dt as dt_util , slugify
3841
3942from . import SystemMonitorConfigEntry
4043from .binary_sensor import BINARY_SENSOR_DOMAIN
5558
5659SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update"
5760
61+ BATTERY_REMAIN_UNKNOWNS = (POWER_TIME_UNKNOWN , POWER_TIME_UNLIMITED )
62+
5863SENSORS_NO_ARG = (
64+ "battery_empty" ,
65+ "battery" ,
5966 "last_boot" ,
6067 "load_" ,
6168 "memory_" ,
6471)
6572SENSORS_WITH_ARG = {
6673 "disk_" : "disk_arguments" ,
74+ "fan_speed" : "fan_speed_arguments" ,
6775 "ipv" : "network_arguments" ,
6876 "process_num_fds" : "processes" ,
6977 ** dict .fromkeys (NET_IO_TYPES , "network_arguments" ),
@@ -139,6 +147,17 @@ def get_process_num_fds(entity: SystemMonitorSensor) -> int | None:
139147 return process_fds .get (entity .argument )
140148
141149
150+ def battery_time_ends (entity : SystemMonitorSensor ) -> datetime | None :
151+ """Return when battery runs out, rounded to minute."""
152+ battery = entity .coordinator .data .battery
153+ if not battery or battery .secsleft in BATTERY_REMAIN_UNKNOWNS :
154+ return None
155+
156+ return (dt_util .utcnow () + timedelta (seconds = battery .secsleft )).replace (
157+ second = 0 , microsecond = 0
158+ )
159+
160+
142161@dataclass (frozen = True , kw_only = True )
143162class SysMonitorSensorEntityDescription (SensorEntityDescription ):
144163 """Describes System Monitor sensor entities."""
@@ -151,6 +170,28 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):
151170
152171
153172SENSOR_TYPES : dict [str , SysMonitorSensorEntityDescription ] = {
173+ "battery" : SysMonitorSensorEntityDescription (
174+ key = "battery" ,
175+ native_unit_of_measurement = PERCENTAGE ,
176+ device_class = SensorDeviceClass .BATTERY ,
177+ state_class = SensorStateClass .MEASUREMENT ,
178+ value_fn = (
179+ lambda entity : entity .coordinator .data .battery .percent
180+ if entity .coordinator .data .battery
181+ else None
182+ ),
183+ none_is_unavailable = True ,
184+ add_to_update = lambda entity : ("battery" , "" ),
185+ ),
186+ "battery_empty" : SysMonitorSensorEntityDescription (
187+ key = "battery_empty" ,
188+ translation_key = "battery_empty" ,
189+ device_class = SensorDeviceClass .TIMESTAMP ,
190+ state_class = SensorStateClass .MEASUREMENT ,
191+ value_fn = battery_time_ends ,
192+ none_is_unavailable = True ,
193+ add_to_update = lambda entity : ("battery" , "" ),
194+ ),
154195 "disk_free" : SysMonitorSensorEntityDescription (
155196 key = "disk_free" ,
156197 translation_key = "disk_free" ,
@@ -199,6 +240,16 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):
199240 none_is_unavailable = True ,
200241 add_to_update = lambda entity : ("disks" , entity .argument ),
201242 ),
243+ "fan_speed" : SysMonitorSensorEntityDescription (
244+ key = "fan_speed" ,
245+ translation_key = "fan_speed" ,
246+ placeholder = "fan_name" ,
247+ native_unit_of_measurement = REVOLUTIONS_PER_MINUTE ,
248+ state_class = SensorStateClass .MEASUREMENT ,
249+ value_fn = lambda entity : entity .coordinator .data .fan_speed [entity .argument ],
250+ none_is_unavailable = True ,
251+ add_to_update = lambda entity : ("fan_speed" , "" ),
252+ ),
202253 "ipv4_address" : SysMonitorSensorEntityDescription (
203254 key = "ipv4_address" ,
204255 translation_key = "ipv4_address" ,
@@ -252,8 +303,8 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):
252303 native_unit_of_measurement = UnitOfInformation .MEBIBYTES ,
253304 device_class = SensorDeviceClass .DATA_SIZE ,
254305 state_class = SensorStateClass .MEASUREMENT ,
255- value_fn = lambda entity : round (
256- entity .coordinator .data .memory .available / 1024 ** 2 , 1
306+ value_fn = (
307+ lambda entity : round ( entity .coordinator .data .memory .available / 1024 ** 2 , 1 )
257308 ),
258309 add_to_update = lambda entity : ("memory" , "" ),
259310 ),
@@ -454,6 +505,7 @@ def get_arguments() -> dict[str, Any]:
454505 return {
455506 "disk_arguments" : get_all_disk_mounts (hass , psutil_wrapper ),
456507 "network_arguments" : get_all_network_interfaces (hass , psutil_wrapper ),
508+ "fan_speed_arguments" : list (sensor_data .fan_speed ),
457509 }
458510
459511 cpu_temperature : float | None = None
0 commit comments