Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions homeassistant/components/smhi/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"name": "Fuel drying",
"state": {
"dry": "Dry",
"extremely_dry": "Extemely dry",
"moderate_wet": "Moderate wet",
"extremely_dry": "Extremely dry",
"moderate_wet": "Moderately wet",
"very_dry": "Very dry",
"very_wet": "Very wet",
"wet": "Wet"
Expand Down
34 changes: 30 additions & 4 deletions homeassistant/components/systemmonitor/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,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",
Expand All @@ -81,6 +81,20 @@ class SysMonitorBinarySensorEntityDescription(BinarySensorEntityDescription):
),
)

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", ""),
entity_registry_enabled_default=False,
),
)


async def async_setup_entry(
hass: HomeAssistant,
Expand All @@ -90,18 +104,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(
Expand Down
34 changes: 33 additions & 1 deletion homeassistant/components/systemmonitor/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import TYPE_CHECKING, Any, NamedTuple

from psutil import Process
from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap
from psutil._common import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap
import psutil_home_assistant as ha_psutil

from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
Expand All @@ -22,6 +22,7 @@

if TYPE_CHECKING:
from . import SystemMonitorConfigEntry
from .util import read_fan_speed

_LOGGER = logging.getLogger(__name__)

Expand All @@ -31,9 +32,11 @@ class SensorData:
"""Sensor data."""

addresses: dict[str, list[snicaddr]]
battery: sbattery | None
boot_time: datetime
cpu_percent: float | None
disk_usage: dict[str, sdiskusage]
fan_speed: dict[str, int]
io_counters: dict[str, snetio]
load: tuple[float, float, float]
memory: VirtualMemory
Expand All @@ -50,17 +53,23 @@ def as_dict(self) -> dict[str, Any]:
disk_usage = None
if self.disk_usage:
disk_usage = {k: str(v) for k, v in self.disk_usage.items()}
fan_speed = None
if self.fan_speed:
fan_speed = {k: str(v) for k, v in self.fan_speed.items()}
io_counters = None
if self.io_counters:
io_counters = {k: str(v) for k, v in self.io_counters.items()}
temperatures = None
if self.temperatures:
temperatures = {k: str(v) for k, v in self.temperatures.items()}

return {
"addresses": addresses,
"battery": str(self.battery),
"boot_time": str(self.boot_time),
"cpu_percent": str(self.cpu_percent),
"disk_usage": disk_usage,
"fan_speed": fan_speed,
"io_counters": io_counters,
"load": str(self.load),
"memory": str(self.memory),
Expand Down Expand Up @@ -125,8 +134,10 @@ def set_subscribers_tuples(
return {
**_disk_defaults,
("addresses", ""): set(),
("battery", ""): set(),
("boot", ""): set(),
("cpu_percent", ""): set(),
("fan_speed", ""): set(),
("io_counters", ""): set(),
("load", ""): set(),
("memory", ""): set(),
Expand Down Expand Up @@ -154,9 +165,11 @@ async def _async_update_data(self) -> SensorData:
self._initial_update = False
return SensorData(
addresses=_data["addresses"],
battery=_data["battery"],
boot_time=_data["boot_time"],
cpu_percent=cpu_percent,
disk_usage=_data["disks"],
fan_speed=_data["fan_speed"],
io_counters=_data["io_counters"],
load=load,
memory=_data["memory"],
Expand Down Expand Up @@ -255,10 +268,29 @@ def update_data(self) -> dict[str, Any]:
except AttributeError:
_LOGGER.debug("OS does not provide temperature sensors")

fan_speed: dict[str, int] = {}
if self.update_subscribers[("fan_speed", "")] or self._initial_update:
try:
fan_sensors = self._psutil.sensors_fans()
fan_speed = read_fan_speed(fan_sensors)
_LOGGER.debug("fan_speed: %s", fan_speed)
except AttributeError:
_LOGGER.debug("OS does not provide fan sensors")

battery: sbattery | None = None
if self.update_subscribers[("battery", "")] or self._initial_update:
try:
battery = self._psutil.sensors_battery()
_LOGGER.debug("battery: %s", battery)
except AttributeError:
_LOGGER.debug("OS does not provide battery sensors")

return {
"addresses": addresses,
"battery": battery,
"boot_time": self.boot_time,
"disks": disks,
"fan_speed": fan_speed,
"io_counters": io_counters,
"memory": memory,
"process_fds": process_fds,
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/systemmonitor/icons.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"entity": {
"sensor": {
"battery_empty": {
"default": "mdi:battery-clock"
},
"disk_free": {
"default": "mdi:harddisk"
},
Expand All @@ -10,6 +13,9 @@
"disk_use_percent": {
"default": "mdi:harddisk"
},
"fan_speed": {
"default": "mdi:fan"
},
"ipv4_address": {
"default": "mdi:ip-network"
},
Expand Down
60 changes: 56 additions & 4 deletions homeassistant/components/systemmonitor/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Callable
import contextlib
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timedelta
from functools import lru_cache
import ipaddress
import logging
Expand All @@ -14,6 +14,8 @@
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,
Expand All @@ -23,6 +25,7 @@
)
from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
UnitOfDataRate,
UnitOfInformation,
Expand All @@ -34,7 +37,7 @@
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from homeassistant.util import dt as dt_util, slugify

from . import SystemMonitorConfigEntry
from .binary_sensor import BINARY_SENSOR_DOMAIN
Expand All @@ -55,7 +58,11 @@

SIGNAL_SYSTEMMONITOR_UPDATE = "systemmonitor_update"

BATTERY_REMAIN_UNKNOWNS = (POWER_TIME_UNKNOWN, POWER_TIME_UNLIMITED)

SENSORS_NO_ARG = (
"battery_empty",
"battery",
"last_boot",
"load_",
"memory_",
Expand All @@ -64,6 +71,7 @@
)
SENSORS_WITH_ARG = {
"disk_": "disk_arguments",
"fan_speed": "fan_speed_arguments",
"ipv": "network_arguments",
"process_num_fds": "processes",
**dict.fromkeys(NET_IO_TYPES, "network_arguments"),
Expand Down Expand Up @@ -139,6 +147,17 @@ def get_process_num_fds(entity: SystemMonitorSensor) -> int | None:
return process_fds.get(entity.argument)


def battery_time_ends(entity: SystemMonitorSensor) -> datetime | None:
"""Return when battery runs out, rounded to minute."""
battery = entity.coordinator.data.battery
if not battery or battery.secsleft in BATTERY_REMAIN_UNKNOWNS:
return None

return (dt_util.utcnow() + timedelta(seconds=battery.secsleft)).replace(
second=0, microsecond=0
)


@dataclass(frozen=True, kw_only=True)
class SysMonitorSensorEntityDescription(SensorEntityDescription):
"""Describes System Monitor sensor entities."""
Expand All @@ -151,6 +170,28 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):


SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
"battery": SysMonitorSensorEntityDescription(
key="battery",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
value_fn=(
lambda entity: entity.coordinator.data.battery.percent
if entity.coordinator.data.battery
else None
),
none_is_unavailable=True,
add_to_update=lambda entity: ("battery", ""),
),
"battery_empty": SysMonitorSensorEntityDescription(
key="battery_empty",
translation_key="battery_empty",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
value_fn=battery_time_ends,
none_is_unavailable=True,
add_to_update=lambda entity: ("battery", ""),
),
"disk_free": SysMonitorSensorEntityDescription(
key="disk_free",
translation_key="disk_free",
Expand Down Expand Up @@ -199,6 +240,16 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):
none_is_unavailable=True,
add_to_update=lambda entity: ("disks", entity.argument),
),
"fan_speed": SysMonitorSensorEntityDescription(
key="fan_speed",
translation_key="fan_speed",
placeholder="fan_name",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: entity.coordinator.data.fan_speed[entity.argument],
none_is_unavailable=True,
add_to_update=lambda entity: ("fan_speed", ""),
),
"ipv4_address": SysMonitorSensorEntityDescription(
key="ipv4_address",
translation_key="ipv4_address",
Expand Down Expand Up @@ -252,8 +303,8 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
device_class=SensorDeviceClass.DATA_SIZE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda entity: round(
entity.coordinator.data.memory.available / 1024**2, 1
value_fn=(
lambda entity: round(entity.coordinator.data.memory.available / 1024**2, 1)
),
add_to_update=lambda entity: ("memory", ""),
),
Expand Down Expand Up @@ -454,6 +505,7 @@ def get_arguments() -> dict[str, Any]:
return {
"disk_arguments": get_all_disk_mounts(hass, psutil_wrapper),
"network_arguments": get_all_network_interfaces(hass, psutil_wrapper),
"fan_speed_arguments": list(sensor_data.fan_speed),
}

cpu_temperature: float | None = None
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/systemmonitor/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
}
},
"sensor": {
"battery_empty": {
"name": "Battery empty"
},
"disk_free": {
"name": "Disk free {mount_point}"
},
Expand All @@ -25,6 +28,9 @@
"disk_use_percent": {
"name": "Disk usage {mount_point}"
},
"fan_speed": {
"name": "{fan_name} fan speed"
},
"ipv4_address": {
"name": "IPv4 address {ip_address}"
},
Expand Down
18 changes: 17 additions & 1 deletion homeassistant/components/systemmonitor/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import os

from psutil._common import shwtemp
from psutil._common import sfan, shwtemp
import psutil_home_assistant as ha_psutil

from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -89,3 +89,19 @@ def read_cpu_temperature(temps: dict[str, list[shwtemp]]) -> float | None:
return round(entry.current, 1)

return None


def read_fan_speed(fans: dict[str, list[sfan]]) -> dict[str, int]:
"""Attempt to read fan speed."""
entry: sfan

_LOGGER.debug("Fan speed: %s", fans)
if not fans:
return {}
sensor_fans: dict[str, int] = {}
for name, entries in fans.items():
for entry in entries:
_label = name if not entry.label else entry.label
sensor_fans[_label] = round(entry.current, 0)

return sensor_fans
Loading
Loading