Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for System Bridge sensors."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import UTC, datetime, timedelta
8 from typing import Final, cast
9 
10 from systembridgemodels.modules.cpu import PerCPU
11 from systembridgemodels.modules.displays import Display
12 from systembridgemodels.modules.gpus import GPU
13 
15  SensorDeviceClass,
16  SensorEntity,
17  SensorEntityDescription,
18  SensorStateClass,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  CONF_PORT,
23  PERCENTAGE,
24  REVOLUTIONS_PER_MINUTE,
25  UnitOfElectricPotential,
26  UnitOfFrequency,
27  UnitOfInformation,
28  UnitOfPower,
29  UnitOfTemperature,
30 )
31 from homeassistant.core import HomeAssistant
32 from homeassistant.helpers.entity_platform import AddEntitiesCallback
33 from homeassistant.helpers.typing import UNDEFINED, StateType
34 from homeassistant.util import dt as dt_util
35 
36 from .const import DOMAIN
37 from .coordinator import SystemBridgeDataUpdateCoordinator
38 from .data import SystemBridgeData
39 from .entity import SystemBridgeEntity
40 
41 ATTR_AVAILABLE: Final = "available"
42 ATTR_FILESYSTEM: Final = "filesystem"
43 ATTR_MOUNT: Final = "mount"
44 ATTR_SIZE: Final = "size"
45 ATTR_TYPE: Final = "type"
46 ATTR_USED: Final = "used"
47 
48 PIXELS: Final = "px"
49 
50 
51 @dataclass(frozen=True)
53  """Class describing System Bridge sensor entities."""
54 
55  value: Callable = round
56 
57 
58 def battery_time_remaining(data: SystemBridgeData) -> datetime | None:
59  """Return the battery time remaining."""
60  if (battery_time := data.battery.time_remaining) is not None:
61  return dt_util.utcnow() + timedelta(seconds=battery_time)
62  return None
63 
64 
65 def cpu_speed(data: SystemBridgeData) -> float | None:
66  """Return the CPU speed."""
67  if (cpu_frequency := data.cpu.frequency) is not None and (
68  cpu_frequency.current
69  ) is not None:
70  return round(cpu_frequency.current / 1000, 2)
71  return None
72 
73 
74 def with_per_cpu(func) -> Callable:
75  """Wrap a function to ensure per CPU data is available."""
76 
77  def wrapper(data: SystemBridgeData, index: int) -> float | None:
78  """Wrap a function to ensure per CPU data is available."""
79  if data.cpu.per_cpu is not None and index < len(data.cpu.per_cpu):
80  return func(data.cpu.per_cpu[index])
81  return None
82 
83  return wrapper
84 
85 
86 @with_per_cpu
87 def cpu_power_per_cpu(per_cpu: PerCPU) -> float | None:
88  """Return CPU power per CPU."""
89  return per_cpu.power
90 
91 
92 @with_per_cpu
93 def cpu_usage_per_cpu(per_cpu: PerCPU) -> float | None:
94  """Return CPU usage per CPU."""
95  return per_cpu.usage
96 
97 
98 def with_display(func) -> Callable:
99  """Wrap a function to ensure a Display is available."""
100 
101  def wrapper(data: SystemBridgeData, index: int) -> Display | None:
102  """Wrap a function to ensure a Display is available."""
103  if index < len(data.displays):
104  return func(data.displays[index])
105  return None
106 
107  return wrapper
108 
109 
110 @with_display
111 def display_resolution_horizontal(display: Display) -> int | None:
112  """Return the Display resolution horizontal."""
113  return display.resolution_horizontal
114 
115 
116 @with_display
117 def display_resolution_vertical(display: Display) -> int | None:
118  """Return the Display resolution vertical."""
119  return display.resolution_vertical
120 
121 
122 @with_display
123 def display_refresh_rate(display: Display) -> float | None:
124  """Return the Display refresh rate."""
125  return display.refresh_rate
126 
127 
128 def with_gpu(func) -> Callable:
129  """Wrap a function to ensure a GPU is available."""
130 
131  def wrapper(data: SystemBridgeData, index: int) -> GPU | None:
132  """Wrap a function to ensure a GPU is available."""
133  if index < len(data.gpus):
134  return func(data.gpus[index])
135  return None
136 
137  return wrapper
138 
139 
140 @with_gpu
141 def gpu_core_clock_speed(gpu: GPU) -> float | None:
142  """Return the GPU core clock speed."""
143  return gpu.core_clock
144 
145 
146 @with_gpu
147 def gpu_fan_speed(gpu: GPU) -> float | None:
148  """Return the GPU fan speed."""
149  return gpu.fan_speed
150 
151 
152 @with_gpu
153 def gpu_memory_clock_speed(gpu: GPU) -> float | None:
154  """Return the GPU memory clock speed."""
155  return gpu.memory_clock
156 
157 
158 @with_gpu
159 def gpu_memory_free(gpu: GPU) -> float | None:
160  """Return the free GPU memory."""
161  return gpu.memory_free
162 
163 
164 @with_gpu
165 def gpu_memory_used(gpu: GPU) -> float | None:
166  """Return the used GPU memory."""
167  return gpu.memory_used
168 
169 
170 @with_gpu
171 def gpu_memory_used_percentage(gpu: GPU) -> float | None:
172  """Return the used GPU memory percentage."""
173  if (gpu.memory_used) is not None and (gpu.memory_total) is not None:
174  return round(gpu.memory_used / gpu.memory_total * 100, 2)
175  return None
176 
177 
178 @with_gpu
179 def gpu_power_usage(gpu: GPU) -> float | None:
180  """Return the GPU power usage."""
181  return gpu.power_usage
182 
183 
184 @with_gpu
185 def gpu_temperature(gpu: GPU) -> float | None:
186  """Return the GPU temperature."""
187  return gpu.temperature
188 
189 
190 @with_gpu
191 def gpu_usage_percentage(gpu: GPU) -> float | None:
192  """Return the GPU usage percentage."""
193  return gpu.core_load
194 
195 
196 def memory_free(data: SystemBridgeData) -> float | None:
197  """Return the free memory."""
198  if (virtual := data.memory.virtual) is not None and (
199  free := virtual.free
200  ) is not None:
201  return round(free / 1000**3, 2)
202  return None
203 
204 
205 def memory_used(data: SystemBridgeData) -> float | None:
206  """Return the used memory."""
207  if (virtual := data.memory.virtual) is not None and (
208  used := virtual.used
209  ) is not None:
210  return round(used / 1000**3, 2)
211  return None
212 
213 
215  data: SystemBridgeData,
216  device_index: int,
217  partition_index: int,
218 ) -> float | None:
219  """Return the used memory."""
220  if (
221  (devices := data.disks.devices) is not None
222  and device_index < len(devices)
223  and (partitions := devices[device_index].partitions) is not None
224  and partition_index < len(partitions)
225  and (usage := partitions[partition_index].usage) is not None
226  ):
227  return usage.percent
228  return None
229 
230 
231 BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
233  key="boot_time",
234  translation_key="boot_time",
235  device_class=SensorDeviceClass.TIMESTAMP,
236  icon="mdi:av-timer",
237  value=lambda data: datetime.fromtimestamp(data.system.boot_time, tz=UTC),
238  ),
240  key="cpu_power_package",
241  translation_key="cpu_power_package",
242  native_unit_of_measurement=UnitOfPower.WATT,
243  state_class=SensorStateClass.MEASUREMENT,
244  suggested_display_precision=2,
245  icon="mdi:chip",
246  value=lambda data: data.cpu.power,
247  ),
249  key="cpu_speed",
250  translation_key="cpu_speed",
251  state_class=SensorStateClass.MEASUREMENT,
252  native_unit_of_measurement=UnitOfFrequency.GIGAHERTZ,
253  device_class=SensorDeviceClass.FREQUENCY,
254  icon="mdi:speedometer",
255  value=cpu_speed,
256  ),
258  key="cpu_temperature",
259  translation_key="cpu_temperature",
260  entity_registry_enabled_default=False,
261  device_class=SensorDeviceClass.TEMPERATURE,
262  state_class=SensorStateClass.MEASUREMENT,
263  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
264  value=lambda data: data.cpu.temperature,
265  ),
267  key="cpu_voltage",
268  translation_key="cpu_voltage",
269  entity_registry_enabled_default=False,
270  device_class=SensorDeviceClass.VOLTAGE,
271  state_class=SensorStateClass.MEASUREMENT,
272  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
273  value=lambda data: data.cpu.voltage,
274  ),
276  key="kernel",
277  translation_key="kernel",
278  icon="mdi:devices",
279  value=lambda data: data.system.platform,
280  ),
282  key="memory_free",
283  translation_key="memory_free",
284  state_class=SensorStateClass.MEASUREMENT,
285  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
286  device_class=SensorDeviceClass.DATA_SIZE,
287  icon="mdi:memory",
288  value=memory_free,
289  ),
291  key="memory_used_percentage",
292  state_class=SensorStateClass.MEASUREMENT,
293  native_unit_of_measurement=PERCENTAGE,
294  icon="mdi:memory",
295  value=lambda data: data.memory.virtual.percent,
296  ),
298  key="memory_used",
299  translation_key="memory_used",
300  entity_registry_enabled_default=False,
301  state_class=SensorStateClass.MEASUREMENT,
302  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
303  device_class=SensorDeviceClass.DATA_SIZE,
304  icon="mdi:memory",
305  value=memory_used,
306  ),
308  key="os",
309  translation_key="os",
310  icon="mdi:devices",
311  value=lambda data: f"{data.system.platform} {data.system.platform_version}",
312  ),
314  key="processes_count",
315  translation_key="processes",
316  state_class=SensorStateClass.MEASUREMENT,
317  icon="mdi:counter",
318  value=lambda data: len(data.processes),
319  ),
321  key="processes_load",
322  translation_key="load",
323  state_class=SensorStateClass.MEASUREMENT,
324  native_unit_of_measurement=PERCENTAGE,
325  icon="mdi:percent",
326  value=lambda data: data.cpu.usage,
327  ),
329  key="version",
330  translation_key="version",
331  icon="mdi:counter",
332  value=lambda data: data.system.version,
333  ),
335  key="version_latest",
336  translation_key="version_latest",
337  icon="mdi:counter",
338  value=lambda data: data.system.version_latest,
339  ),
340 )
341 
342 BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
344  key="battery",
345  device_class=SensorDeviceClass.BATTERY,
346  state_class=SensorStateClass.MEASUREMENT,
347  native_unit_of_measurement=PERCENTAGE,
348  value=lambda data: data.battery.percentage,
349  ),
351  key="battery_time_remaining",
352  translation_key="battery_time_remaining",
353  device_class=SensorDeviceClass.TIMESTAMP,
354  value=battery_time_remaining,
355  ),
356 )
357 
358 
360  hass: HomeAssistant,
361  entry: ConfigEntry,
362  async_add_entities: AddEntitiesCallback,
363 ) -> None:
364  """Set up System Bridge sensor based on a config entry."""
365  coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
366 
367  entities = [
368  SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])
369  for description in BASE_SENSOR_TYPES
370  ]
371 
372  for index_device, device in enumerate(coordinator.data.disks.devices):
373  if device.partitions is None:
374  continue
375 
376  entities.extend(
378  coordinator,
380  key=f"filesystem_{partition.mount_point.replace(':', '')}",
381  name=f"{partition.mount_point} space used",
382  state_class=SensorStateClass.MEASUREMENT,
383  native_unit_of_measurement=PERCENTAGE,
384  icon="mdi:harddisk",
385  value=(
386  lambda data,
387  dk=index_device,
388  pk=index_partition: partition_usage(data, dk, pk)
389  ),
390  ),
391  entry.data[CONF_PORT],
392  )
393  for index_partition, partition in enumerate(device.partitions)
394  )
395 
396  if (
397  coordinator.data.battery
398  and coordinator.data.battery.percentage
399  and coordinator.data.battery.percentage > -1
400  ):
401  entities.extend(
402  SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT])
403  for description in BATTERY_SENSOR_TYPES
404  )
405 
406  entities.append(
408  coordinator,
410  key="displays_connected",
411  translation_key="displays_connected",
412  state_class=SensorStateClass.MEASUREMENT,
413  icon="mdi:monitor",
414  value=lambda data: len(data.displays) if data.displays else None,
415  ),
416  entry.data[CONF_PORT],
417  )
418  )
419 
420  if coordinator.data.displays is not None:
421  for index, display in enumerate(coordinator.data.displays):
422  entities = [
423  *entities,
425  coordinator,
427  key=f"display_{display.id}_resolution_x",
428  name=f"Display {display.id} resolution x",
429  state_class=SensorStateClass.MEASUREMENT,
430  native_unit_of_measurement=PIXELS,
431  icon="mdi:monitor",
432  value=lambda data, k=index: display_resolution_horizontal(
433  data, k
434  ),
435  ),
436  entry.data[CONF_PORT],
437  ),
439  coordinator,
441  key=f"display_{display.id}_resolution_y",
442  name=f"Display {display.id} resolution y",
443  state_class=SensorStateClass.MEASUREMENT,
444  native_unit_of_measurement=PIXELS,
445  icon="mdi:monitor",
446  value=lambda data, k=index: display_resolution_vertical(
447  data, k
448  ),
449  ),
450  entry.data[CONF_PORT],
451  ),
453  coordinator,
455  key=f"display_{display.id}_refresh_rate",
456  name=f"Display {display.id} refresh rate",
457  state_class=SensorStateClass.MEASUREMENT,
458  native_unit_of_measurement=UnitOfFrequency.HERTZ,
459  device_class=SensorDeviceClass.FREQUENCY,
460  icon="mdi:monitor",
461  value=lambda data, k=index: display_refresh_rate(data, k),
462  ),
463  entry.data[CONF_PORT],
464  ),
465  ]
466 
467  for index, gpu in enumerate(coordinator.data.gpus):
468  entities.extend(
469  [
471  coordinator,
473  key=f"gpu_{gpu.id}_core_clock_speed",
474  name=f"{gpu.name} clock speed",
475  entity_registry_enabled_default=False,
476  state_class=SensorStateClass.MEASUREMENT,
477  native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
478  device_class=SensorDeviceClass.FREQUENCY,
479  icon="mdi:speedometer",
480  value=lambda data, k=index: gpu_core_clock_speed(data, k),
481  ),
482  entry.data[CONF_PORT],
483  ),
485  coordinator,
487  key=f"gpu_{gpu.id}_memory_clock_speed",
488  name=f"{gpu.name} memory clock speed",
489  entity_registry_enabled_default=False,
490  state_class=SensorStateClass.MEASUREMENT,
491  native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
492  device_class=SensorDeviceClass.FREQUENCY,
493  icon="mdi:speedometer",
494  value=lambda data, k=index: gpu_memory_clock_speed(data, k),
495  ),
496  entry.data[CONF_PORT],
497  ),
499  coordinator,
501  key=f"gpu_{gpu.id}_memory_free",
502  name=f"{gpu.name} memory free",
503  state_class=SensorStateClass.MEASUREMENT,
504  native_unit_of_measurement=UnitOfInformation.MEGABYTES,
505  device_class=SensorDeviceClass.DATA_SIZE,
506  icon="mdi:memory",
507  value=lambda data, k=index: gpu_memory_free(data, k),
508  ),
509  entry.data[CONF_PORT],
510  ),
512  coordinator,
514  key=f"gpu_{gpu.id}_memory_used_percentage",
515  name=f"{gpu.name} memory used %",
516  state_class=SensorStateClass.MEASUREMENT,
517  native_unit_of_measurement=PERCENTAGE,
518  icon="mdi:memory",
519  value=lambda data, k=index: gpu_memory_used_percentage(data, k),
520  ),
521  entry.data[CONF_PORT],
522  ),
524  coordinator,
526  key=f"gpu_{gpu.id}_memory_used",
527  name=f"{gpu.name} memory used",
528  entity_registry_enabled_default=False,
529  state_class=SensorStateClass.MEASUREMENT,
530  native_unit_of_measurement=UnitOfInformation.MEGABYTES,
531  device_class=SensorDeviceClass.DATA_SIZE,
532  icon="mdi:memory",
533  value=lambda data, k=index: gpu_memory_used(data, k),
534  ),
535  entry.data[CONF_PORT],
536  ),
538  coordinator,
540  key=f"gpu_{gpu.id}_fan_speed",
541  name=f"{gpu.name} fan speed",
542  entity_registry_enabled_default=False,
543  state_class=SensorStateClass.MEASUREMENT,
544  native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
545  icon="mdi:fan",
546  value=lambda data, k=index: gpu_fan_speed(data, k),
547  ),
548  entry.data[CONF_PORT],
549  ),
551  coordinator,
553  key=f"gpu_{gpu.id}_power_usage",
554  name=f"{gpu.name} power usage",
555  entity_registry_enabled_default=False,
556  device_class=SensorDeviceClass.POWER,
557  state_class=SensorStateClass.MEASUREMENT,
558  native_unit_of_measurement=UnitOfPower.WATT,
559  value=lambda data, k=index: gpu_power_usage(data, k),
560  ),
561  entry.data[CONF_PORT],
562  ),
564  coordinator,
566  key=f"gpu_{gpu.id}_temperature",
567  name=f"{gpu.name} temperature",
568  entity_registry_enabled_default=False,
569  device_class=SensorDeviceClass.TEMPERATURE,
570  state_class=SensorStateClass.MEASUREMENT,
571  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
572  value=lambda data, k=index: gpu_temperature(data, k),
573  ),
574  entry.data[CONF_PORT],
575  ),
577  coordinator,
579  key=f"gpu_{gpu.id}_usage_percentage",
580  name=f"{gpu.name} usage %",
581  state_class=SensorStateClass.MEASUREMENT,
582  native_unit_of_measurement=PERCENTAGE,
583  icon="mdi:percent",
584  value=lambda data, k=index: gpu_usage_percentage(data, k),
585  ),
586  entry.data[CONF_PORT],
587  ),
588  ]
589  )
590 
591  if coordinator.data.cpu.per_cpu is not None:
592  for cpu in coordinator.data.cpu.per_cpu:
593  entities.extend(
594  [
596  coordinator,
598  key=f"processes_load_cpu_{cpu.id}",
599  name=f"Load CPU {cpu.id}",
600  entity_registry_enabled_default=False,
601  state_class=SensorStateClass.MEASUREMENT,
602  native_unit_of_measurement=PERCENTAGE,
603  icon="mdi:percent",
604  value=lambda data, k=cpu.id: cpu_usage_per_cpu(data, k),
605  ),
606  entry.data[CONF_PORT],
607  ),
609  coordinator,
611  key=f"cpu_power_core_{cpu.id}",
612  name=f"CPU Core {cpu.id} Power",
613  entity_registry_enabled_default=False,
614  native_unit_of_measurement=UnitOfPower.WATT,
615  state_class=SensorStateClass.MEASUREMENT,
616  icon="mdi:chip",
617  value=lambda data, k=cpu.id: cpu_power_per_cpu(data, k),
618  ),
619  entry.data[CONF_PORT],
620  ),
621  ]
622  )
623 
624  async_add_entities(entities)
625 
626 
628  """Define a System Bridge sensor."""
629 
630  entity_description: SystemBridgeSensorEntityDescription
631 
632  def __init__(
633  self,
634  coordinator: SystemBridgeDataUpdateCoordinator,
635  description: SystemBridgeSensorEntityDescription,
636  api_port: int,
637  ) -> None:
638  """Initialize."""
639  super().__init__(
640  coordinator,
641  api_port,
642  description.key,
643  )
644  self.entity_descriptionentity_description = description
645  if description.name != UNDEFINED:
646  self._attr_has_entity_name_attr_has_entity_name_attr_has_entity_name = False
647 
648  @property
649  def native_value(self) -> StateType:
650  """Return the state."""
651  try:
652  return cast(StateType, self.entity_descriptionentity_description.value(self.coordinator.data))
653  except TypeError:
654  return None
None __init__(self, SystemBridgeDataUpdateCoordinator coordinator, SystemBridgeSensorEntityDescription description, int api_port)
Definition: sensor.py:637
float|None memory_used(SystemBridgeData data)
Definition: sensor.py:205
float|None memory_free(SystemBridgeData data)
Definition: sensor.py:196
float|None cpu_power_per_cpu(PerCPU per_cpu)
Definition: sensor.py:87
int|None display_resolution_vertical(Display display)
Definition: sensor.py:117
float|None cpu_speed(SystemBridgeData data)
Definition: sensor.py:65
int|None display_resolution_horizontal(Display display)
Definition: sensor.py:111
float|None display_refresh_rate(Display display)
Definition: sensor.py:123
float|None gpu_memory_clock_speed(GPU gpu)
Definition: sensor.py:153
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:363
datetime|None battery_time_remaining(SystemBridgeData data)
Definition: sensor.py:58
float|None gpu_memory_used_percentage(GPU gpu)
Definition: sensor.py:171
float|None partition_usage(SystemBridgeData data, int device_index, int partition_index)
Definition: sensor.py:218
float|None cpu_usage_per_cpu(PerCPU per_cpu)
Definition: sensor.py:93