1 """Support for monitoring the local system."""
3 from __future__
import annotations
5 from collections.abc
import Callable
7 from dataclasses
import dataclass
8 from datetime
import datetime
9 from functools
import lru_cache
15 from typing
import Any, Literal
18 DOMAIN
as SENSOR_DOMAIN,
21 SensorEntityDescription,
39 from .
import SystemMonitorConfigEntry
40 from .const
import DOMAIN, NET_IO_TYPES
41 from .coordinator
import SystemMonitorCoordinator
42 from .util
import get_all_disk_mounts, get_all_network_interfaces, read_cpu_temperature
44 _LOGGER = logging.getLogger(__name__)
52 SENSOR_TYPE_DEVICE_CLASS = 3
53 SENSOR_TYPE_MANDATORY_ARG = 4
55 SIGNAL_SYSTEMMONITOR_UPDATE =
"systemmonitor_update"
60 """Return cpu icon."""
61 if sys.maxsize > 2**32:
62 return "mdi:cpu-64-bit"
63 return "mdi:cpu-32-bit"
67 """Return network in and out."""
68 counters = entity.coordinator.data.io_counters
69 if entity.argument
in counters:
70 counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
71 return round(counter / 1024**2, 1)
76 """Return packets in and out."""
77 counters = entity.coordinator.data.io_counters
78 if entity.argument
in counters:
79 return counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
84 """Return network throughput in and out."""
85 counters = entity.coordinator.data.io_counters
87 if entity.argument
in counters:
88 counter = counters[entity.argument][IO_COUNTER[entity.entity_description.key]]
89 now = time.monotonic()
91 (value := entity.value)
92 and (update_time := entity.update_time)
96 (counter - value) / 1000**2 / (now - update_time),
99 entity.update_time = now
100 entity.value = counter
105 entity: SystemMonitorSensor,
107 """Return network ip address."""
108 addresses = entity.coordinator.data.addresses
109 if entity.argument
in addresses:
110 for addr
in addresses[entity.argument]:
111 if addr.family == IF_ADDRS_FAMILY[entity.entity_description.key]:
112 address = ipaddress.ip_address(addr.address)
113 if address.version == 6
and (
114 address.is_link_local
or address.is_loopback
121 @dataclass(frozen=True, kw_only=True)
123 """Describes System Monitor sensor entities."""
125 value_fn: Callable[[SystemMonitorSensor], StateType | datetime]
126 add_to_update: Callable[[SystemMonitorSensor], tuple[str, str]]
127 none_is_unavailable: bool =
False
128 mandatory_arg: bool =
False
129 placeholder: str |
None =
None
132 SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = {
135 translation_key=
"disk_free",
136 placeholder=
"mount_point",
137 native_unit_of_measurement=UnitOfInformation.GIBIBYTES,
138 device_class=SensorDeviceClass.DATA_SIZE,
139 state_class=SensorStateClass.MEASUREMENT,
140 value_fn=
lambda entity: round(
141 entity.coordinator.data.disk_usage[entity.argument].free / 1024**3, 1
143 if entity.argument
in entity.coordinator.data.disk_usage
145 none_is_unavailable=
True,
146 add_to_update=
lambda entity: (
"disks", entity.argument),
150 translation_key=
"disk_use",
151 placeholder=
"mount_point",
152 native_unit_of_measurement=UnitOfInformation.GIBIBYTES,
153 device_class=SensorDeviceClass.DATA_SIZE,
154 state_class=SensorStateClass.MEASUREMENT,
155 value_fn=
lambda entity: round(
156 entity.coordinator.data.disk_usage[entity.argument].used / 1024**3, 1
158 if entity.argument
in entity.coordinator.data.disk_usage
160 none_is_unavailable=
True,
161 add_to_update=
lambda entity: (
"disks", entity.argument),
164 key=
"disk_use_percent",
165 translation_key=
"disk_use_percent",
166 placeholder=
"mount_point",
167 native_unit_of_measurement=PERCENTAGE,
168 state_class=SensorStateClass.MEASUREMENT,
169 value_fn=
lambda entity: entity.coordinator.data.disk_usage[
172 if entity.argument
in entity.coordinator.data.disk_usage
174 none_is_unavailable=
True,
175 add_to_update=
lambda entity: (
"disks", entity.argument),
179 translation_key=
"ipv4_address",
180 placeholder=
"ip_address",
182 value_fn=get_ip_address,
183 add_to_update=
lambda entity: (
"addresses",
""),
187 translation_key=
"ipv6_address",
188 placeholder=
"ip_address",
190 value_fn=get_ip_address,
191 add_to_update=
lambda entity: (
"addresses",
""),
195 translation_key=
"last_boot",
196 device_class=SensorDeviceClass.TIMESTAMP,
197 value_fn=
lambda entity: entity.coordinator.data.boot_time,
198 add_to_update=
lambda entity: (
"boot",
""),
202 translation_key=
"load_15m",
204 state_class=SensorStateClass.MEASUREMENT,
205 value_fn=
lambda entity: round(entity.coordinator.data.load[2], 2),
206 add_to_update=
lambda entity: (
"load",
""),
210 translation_key=
"load_1m",
212 state_class=SensorStateClass.MEASUREMENT,
213 value_fn=
lambda entity: round(entity.coordinator.data.load[0], 2),
214 add_to_update=
lambda entity: (
"load",
""),
218 translation_key=
"load_5m",
220 state_class=SensorStateClass.MEASUREMENT,
221 value_fn=
lambda entity: round(entity.coordinator.data.load[1], 2),
222 add_to_update=
lambda entity: (
"load",
""),
226 translation_key=
"memory_free",
227 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
228 device_class=SensorDeviceClass.DATA_SIZE,
229 state_class=SensorStateClass.MEASUREMENT,
230 value_fn=
lambda entity: round(
231 entity.coordinator.data.memory.available / 1024**2, 1
233 add_to_update=
lambda entity: (
"memory",
""),
237 translation_key=
"memory_use",
238 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
239 device_class=SensorDeviceClass.DATA_SIZE,
240 state_class=SensorStateClass.MEASUREMENT,
241 value_fn=
lambda entity: round(
243 entity.coordinator.data.memory.total
244 - entity.coordinator.data.memory.available
249 add_to_update=
lambda entity: (
"memory",
""),
252 key=
"memory_use_percent",
253 translation_key=
"memory_use_percent",
254 native_unit_of_measurement=PERCENTAGE,
255 state_class=SensorStateClass.MEASUREMENT,
256 value_fn=
lambda entity: entity.coordinator.data.memory.percent,
257 add_to_update=
lambda entity: (
"memory",
""),
261 translation_key=
"network_in",
262 placeholder=
"interface",
263 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
264 device_class=SensorDeviceClass.DATA_SIZE,
265 state_class=SensorStateClass.TOTAL_INCREASING,
267 value_fn=get_network,
268 add_to_update=
lambda entity: (
"io_counters",
""),
272 translation_key=
"network_out",
273 placeholder=
"interface",
274 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
275 device_class=SensorDeviceClass.DATA_SIZE,
276 state_class=SensorStateClass.TOTAL_INCREASING,
278 value_fn=get_network,
279 add_to_update=
lambda entity: (
"io_counters",
""),
283 translation_key=
"packets_in",
284 placeholder=
"interface",
285 state_class=SensorStateClass.TOTAL_INCREASING,
287 value_fn=get_packets,
288 add_to_update=
lambda entity: (
"io_counters",
""),
292 translation_key=
"packets_out",
293 placeholder=
"interface",
294 state_class=SensorStateClass.TOTAL_INCREASING,
296 value_fn=get_packets,
297 add_to_update=
lambda entity: (
"io_counters",
""),
300 key=
"throughput_network_in",
301 translation_key=
"throughput_network_in",
302 placeholder=
"interface",
303 native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
304 device_class=SensorDeviceClass.DATA_RATE,
305 state_class=SensorStateClass.MEASUREMENT,
307 value_fn=get_throughput,
308 add_to_update=
lambda entity: (
"io_counters",
""),
311 key=
"throughput_network_out",
312 translation_key=
"throughput_network_out",
313 placeholder=
"interface",
314 native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
315 device_class=SensorDeviceClass.DATA_RATE,
316 state_class=SensorStateClass.MEASUREMENT,
318 value_fn=get_throughput,
319 add_to_update=
lambda entity: (
"io_counters",
""),
323 translation_key=
"processor_use",
324 native_unit_of_measurement=PERCENTAGE,
326 state_class=SensorStateClass.MEASUREMENT,
327 value_fn=
lambda entity: (
328 round(entity.coordinator.data.cpu_percent)
329 if entity.coordinator.data.cpu_percent
332 add_to_update=
lambda entity: (
"cpu_percent",
""),
335 key=
"processor_temperature",
336 translation_key=
"processor_temperature",
337 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
338 device_class=SensorDeviceClass.TEMPERATURE,
339 state_class=SensorStateClass.MEASUREMENT,
341 entity.coordinator.data.temperatures
343 none_is_unavailable=
True,
344 add_to_update=
lambda entity: (
"temperatures",
""),
348 translation_key=
"swap_free",
349 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
350 device_class=SensorDeviceClass.DATA_SIZE,
351 state_class=SensorStateClass.MEASUREMENT,
352 value_fn=
lambda entity: round(entity.coordinator.data.swap.free / 1024**2, 1),
353 add_to_update=
lambda entity: (
"swap",
""),
357 translation_key=
"swap_use",
358 native_unit_of_measurement=UnitOfInformation.MEBIBYTES,
359 device_class=SensorDeviceClass.DATA_SIZE,
360 state_class=SensorStateClass.MEASUREMENT,
361 value_fn=
lambda entity: round(entity.coordinator.data.swap.used / 1024**2, 1),
362 add_to_update=
lambda entity: (
"swap",
""),
365 key=
"swap_use_percent",
366 translation_key=
"swap_use_percent",
367 native_unit_of_measurement=PERCENTAGE,
368 state_class=SensorStateClass.MEASUREMENT,
369 value_fn=
lambda entity: entity.coordinator.data.swap.percent,
370 add_to_update=
lambda entity: (
"swap",
""),
376 """Return True if legacy resource was configured."""
379 if resource
in resources:
380 _LOGGER.debug(
"Checking %s in %s returns True", resource,
", ".join(resources))
382 _LOGGER.debug(
"Checking %s in %s returns False", resource,
", ".join(resources))
391 "throughput_network_out": 0,
392 "throughput_network_in": 1,
394 IF_ADDRS_FAMILY = {
"ipv4_address": socket.AF_INET,
"ipv6_address": socket.AF_INET6}
399 entry: SystemMonitorConfigEntry,
400 async_add_entities: AddEntitiesCallback,
402 """Set up System Monitor sensors based on a config entry."""
403 entities: list[SystemMonitorSensor] = []
404 legacy_resources: set[str] = set(entry.options.get(
"resources", []))
405 loaded_resources: set[str] = set()
406 coordinator = entry.runtime_data.coordinator
407 psutil_wrapper = entry.runtime_data.psutil_wrapper
408 sensor_data = coordinator.data
411 """Return startup information."""
417 cpu_temperature: float |
None =
None
418 with contextlib.suppress(AttributeError):
421 startup_arguments = await hass.async_add_executor_job(get_arguments)
422 startup_arguments[
"cpu_temperature"] = cpu_temperature
424 _LOGGER.debug(
"Setup from options %s", entry.options)
426 for _type, sensor_description
in SENSOR_TYPES.items():
427 if _type.startswith(
"disk_"):
428 for argument
in startup_arguments[
"disk_arguments"]:
430 f
"{_type}_{argument}", legacy_resources
432 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
444 if _type.startswith(
"ipv"):
445 for argument
in startup_arguments[
"network_arguments"]:
447 f
"{_type}_{argument}", legacy_resources
449 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
461 if _type ==
"last_boot":
464 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
476 if _type.startswith(
"load_"):
479 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
491 if _type.startswith(
"memory_"):
494 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
505 if _type
in NET_IO_TYPES:
506 for argument
in startup_arguments[
"network_arguments"]:
508 f
"{_type}_{argument}", legacy_resources
510 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
522 if _type ==
"processor_use":
525 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
537 if _type ==
"processor_temperature":
538 if not startup_arguments[
"cpu_temperature"]:
543 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
555 if _type.startswith(
"swap_"):
558 loaded_resources.add(
slugify(f
"{_type}_{argument}"))
571 for resource
in legacy_resources:
572 if resource.startswith(
"disk_"):
573 check_resource =
slugify(resource)
575 "Check resource %s already loaded in %s",
579 if check_resource
not in loaded_resources:
580 loaded_resources.add(check_resource)
581 split_index = resource.rfind(
"_")
582 _type = resource[:split_index]
583 argument = resource[split_index + 1 :]
584 _LOGGER.debug(
"Loading legacy %s with argument %s", _type, argument)
596 def clean_obsolete_entities() -> None:
597 """Remove entities which are disabled and not supported from setup."""
598 entity_registry = er.async_get(hass)
599 entities = entity_registry.entities.get_entries_for_config_entry_id(
602 for entity
in entities:
604 entity.unique_id
not in loaded_resources
605 and entity.disabled
is True
607 entity_id := entity_registry.async_get_entity_id(
608 SENSOR_DOMAIN, DOMAIN, entity.unique_id
612 entity_registry.async_remove(entity_id)
614 clean_obsolete_entities()
620 """Implementation of a system monitor sensor."""
622 _attr_has_entity_name =
True
623 _attr_entity_category = EntityCategory.DIAGNOSTIC
624 entity_description: SysMonitorSensorEntityDescription
629 coordinator: SystemMonitorCoordinator,
630 sensor_description: SysMonitorSensorEntityDescription,
633 legacy_enabled: bool =
False,
635 """Initialize the sensor."""
642 self._attr_unique_id: str =
slugify(f
"{sensor_description.key}_{argument}")
645 entry_type=DeviceEntryType.SERVICE,
646 identifiers={(DOMAIN, entry_id)},
647 manufacturer=
"System Monitor",
648 name=
"System Monitor",
651 self.value: int |
None =
None
652 self.update_time: float |
None =
None
656 """When added to hass."""
657 self.coordinator.update_subscribers[
663 """When removed from hass."""
664 self.coordinator.update_subscribers[
671 """Handle updated data from the coordinator."""
679 """Return if entity is available."""
682 self.coordinator.last_update_success
is True
685 return super().available
StateType|date|datetime|Decimal native_value(self)
_attr_entity_registry_enabled_default
None __init__(self, SystemMonitorCoordinator coordinator, SysMonitorSensorEntityDescription sensor_description, str entry_id, str argument, bool legacy_enabled=False)
_attr_translation_placeholders
None async_will_remove_from_hass(self)
None _handle_coordinator_update(self)
None async_added_to_hass(self)
argparse.Namespace get_arguments()
bool add(self, _T matcher)
bool remove(self, _T matcher)
bool check_legacy_resource(str resource, set[str] resources)
float|None get_packets(SystemMonitorSensor entity)
float|None get_throughput(SystemMonitorSensor entity)
str|None get_ip_address(SystemMonitorSensor entity)
Literal["mdi:cpu-64-bit", "mdi:cpu-32-bit"] get_cpu_icon()
None async_setup_entry(HomeAssistant hass, SystemMonitorConfigEntry entry, AddEntitiesCallback async_add_entities)
float|None get_network(SystemMonitorSensor entity)
float|None read_cpu_temperature(dict[str, list[shwtemp]] temps)
set[str] get_all_disk_mounts(HomeAssistant hass, ha_psutil.PsutilWrapper psutil_wrapper)
set[str] get_all_network_interfaces(HomeAssistant hass, ha_psutil.PsutilWrapper psutil_wrapper)