summaryrefslogtreecommitdiffstats
path: root/glances/plugins/sensors/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'glances/plugins/sensors/__init__.py')
-rw-r--r--glances/plugins/sensors/__init__.py265
1 files changed, 120 insertions, 145 deletions
diff --git a/glances/plugins/sensors/__init__.py b/glances/plugins/sensors/__init__.py
index ae8e087a..8634d579 100644
--- a/glances/plugins/sensors/__init__.py
+++ b/glances/plugins/sensors/__init__.py
@@ -2,29 +2,44 @@
#
# This file is part of Glances.
#
-# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
+# SPDX-FileCopyrightText: 2024 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Sensors plugin."""
+from enum import Enum
+from concurrent.futures import ThreadPoolExecutor
+from typing import List, Dict, Literal, Any
import psutil
import warnings
from glances.logger import logger
-from glances.globals import iteritems, to_fahrenheit
+from glances.globals import to_fahrenheit
from glances.timer import Counter
from glances.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel
from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.plugin.model import GlancesPluginModel
-SENSOR_TEMP_TYPE = 'temperature_core'
-SENSOR_TEMP_UNIT = 'C'
-SENSOR_FAN_TYPE = 'fan_speed'
-SENSOR_FAN_UNIT = 'R'
+class SensorType(str, Enum):
+ CPU_TEMP = 'temperature_core'
+ FAN_SPEED = 'fan_speed'
+ HDD_TEMP = 'temperature_hdd'
+ BATTERY = 'battery'
+
+
+CPU_TEMP_UNIT = 'C'
+FAN_SPEED_UNIT = 'R'
+HDD_TEMP_UNIT = 'C'
+BATTERY_UNIT = '%'
+
+# Define the default refresh multiplicator
+# Default value is 3 * Glances refresh time
+# Can be overwritten by the refresh option in the sensors section of the glances.conf file
+DEFAULT_REFRESH = 3
# Fields description
# description: human readable description
@@ -56,53 +71,90 @@ fields_description = {
},
}
+
class PluginModel(GlancesPluginModel):
"""Glances sensors plugin.
The stats list includes both sensors and hard disks stats, if any.
- The sensors are already grouped by chip type and then sorted by name.
- The hard disks are already sorted by name.
+ The sensors are already grouped by chip type and then sorted by label.
+ The hard disks are already sorted by label.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
- args=args, config=config,
- stats_init_value=[],
- fields_description=fields_description
+ args=args, config=config, stats_init_value=[], fields_description=fields_description
)
-
start_duration = Counter()
# Init the sensor class
start_duration.reset()
- self.glances_grab_sensors = GlancesGrabSensors()
- logger.debug("Generic sensor plugin init duration: {} seconds".format(start_duration.get()))
+ glances_grab_sensors_cpu_temp = GlancesGrabSensors(SensorType.CPU_TEMP)
+ logger.debug("CPU Temp sensor plugin init duration: {} seconds".format(start_duration.get()))
+
+ start_duration.reset()
+ glances_grab_sensors_fan_speed = GlancesGrabSensors(SensorType.FAN_SPEED)
+ logger.debug("Fan speed sensor plugin init duration: {} seconds".format(start_duration.get()))
- # Instance for the HDDTemp Plugin in order to display the hard disks
- # temperatures
+ # Instance for the HDDTemp Plugin in order to display the hard disks temperatures
start_duration.reset()
- self.hddtemp_plugin = HddTempPluginModel(args=args, config=config)
+ hddtemp_plugin = HddTempPluginModel(args=args, config=config)
logger.debug("HDDTemp sensor plugin init duration: {} seconds".format(start_duration.get()))
- # Instance for the BatPercent in order to display the batteries
- # capacities
+ # Instance for the BatPercent in order to display the batteries capacities
start_duration.reset()
- self.batpercent_plugin = BatPercentPluginModel(args=args, config=config)
+ batpercent_plugin = BatPercentPluginModel(args=args, config=config)
logger.debug("Battery sensor plugin init duration: {} seconds".format(start_duration.get()))
+ self.sensors_grab_map: Dict[SensorType, Any] = {}
+
+ if glances_grab_sensors_cpu_temp.init:
+ self.sensors_grab_map[SensorType.CPU_TEMP] = glances_grab_sensors_cpu_temp
+
+ if glances_grab_sensors_fan_speed.init:
+ self.sensors_grab_map[SensorType.FAN_SPEED] = glances_grab_sensors_fan_speed
+
+ self.sensors_grab_map[SensorType.HDD_TEMP] = hddtemp_plugin
+ self.sensors_grab_map[SensorType.BATTERY] = batpercent_plugin
+
# We want to display the stat in the curse interface
self.display_curse = True
# Not necessary to refresh every refresh time
- # By default set to refresh * 2
- if self.get_refresh() == args.time:
- self.set_refresh(self.get_refresh() * 2)
+ if args and self.get_refresh() == args.time:
+ self.set_refresh(self.get_refresh() * DEFAULT_REFRESH)
def get_key(self):
"""Return the key of the list."""
return 'label'
+ def __get_sensor_data(self, sensor_type: SensorType) -> List[Dict]:
+ try:
+ data = self.sensors_grab_map[sensor_type].update()
+ data = self.__set_type(data, sensor_type)
+ except Exception as e:
+ logger.error(f"Cannot grab sensors `{sensor_type}` ({e})")
+ return []
+ else:
+ return self.__transform_sensors(data)
+
+ def __transform_sensors(self, threads_stats):
+ """Hide, alias and sort the result"""
+ stats_transformed = []
+ for stat in threads_stats:
+ # Hide sensors configured in the hide ou show configuration key
+ if not self.is_display(stat["label"].lower()):
+ continue
+ # Set alias for sensors
+ stat["label"] = self.__get_alias(stat)
+ # Add the stat to the stats_transformed list
+ stats_transformed.append(stat)
+ # Remove duplicates thanks to https://stackoverflow.com/a/9427216/1919431
+ stats_transformed = [dict(t) for t in {tuple(d.items()) for d in stats_transformed}]
+ # Sort by label
+ stats_transformed = sorted(stats_transformed, key=lambda d: d['label'])
+ return stats_transformed
+
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
@@ -111,40 +163,16 @@ class PluginModel(GlancesPluginModel):
stats = self.get_init_value()
if self.input_method == 'local':
- # Update stats using the dedicated lib
- stats = []
- # Get the temperature
- try:
- temperature = self.__set_type(self.glances_grab_sensors.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE)
- except Exception as e:
- logger.error("Cannot grab sensors temperatures (%s)" % e)
- else:
- # Append temperature
- stats.extend(temperature)
- # Get the FAN speed
- try:
- fan_speed = self.__set_type(self.glances_grab_sensors.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE)
- except Exception as e:
- logger.error("Cannot grab FAN speed (%s)" % e)
- else:
- # Append FAN speed
- stats.extend(fan_speed)
- # Update HDDtemp stats
- try:
- hddtemp = self.__set_type(self.hddtemp_plugin.update(), 'temperature_hdd')
- except Exception as e:
- logger.error("Cannot grab HDD temperature (%s)" % e)
- else:
- # Append HDD temperature
- stats.extend(hddtemp)
- # Update batteries stats
- try:
- bat_percent = self.__set_type(self.batpercent_plugin.update(), 'battery')
- except Exception as e:
- logger.error("Cannot grab battery percent (%s)" % e)
- else:
- # Append Batteries %
- stats.extend(bat_percent)
+ with ThreadPoolExecutor(max_workers=len(self.sensors_grab_map)) as executor:
+ logger.debug(f"Sensors enabled sub plugins: {list(self.sensors_grab_map.keys())}")
+ futures = {t: executor.submit(self.__get_sensor_data, t) for t in self.sensors_grab_map.keys()}
+
+ # Merge the results
+ for sensor_type, future in futures.items():
+ try:
+ stats.extend(future.result())
+ except Exception as e:
+ logger.error(f"Cannot parse sensors data for `{sensor_type}` ({e})")
elif self.input_method == 'snmp':
# Update stats using SNMP
@@ -152,19 +180,8 @@ class PluginModel(GlancesPluginModel):
# http://www.net-snmp.org/wiki/index.php/Net-SNMP_and_lm-sensors_on_Ubuntu_10.04
pass
- # Global change on stats
- self.stats = self.get_init_value()
- for stat in stats:
- # Do not take hide stat into account
- if not self.is_display(stat["label"].lower()):
- continue
- # Set the alias for each stat
- # alias = self.has_alias(stat["label"].lower())
- # if alias:
- # stat["label"] = alias
- stat["label"] = self.__get_alias(stat)
- # Update the stats
- self.stats.append(stat)
+ # Update the stats
+ self.stats = stats
return self.stats
@@ -189,7 +206,7 @@ class PluginModel(GlancesPluginModel):
"""
for i in stats:
# Set the sensors type
- i.update({'type': sensor_type})
+ i.update({'type': str(sensor_type)})
# also add the key name
i.update({'key': self.get_key()})
@@ -206,10 +223,10 @@ class PluginModel(GlancesPluginModel):
if not i['value']:
continue
# Alert processing
- if i['type'] == SENSOR_TEMP_TYPE:
- if self.is_limit('critical', stat_name='sensors_temperature_' + i['label']):
+ if i['type'] == SensorType.CPU_TEMP:
+ if self.is_limit('critical', stat_name=SensorType.CPU_TEMP + '_' + i['label']):
# By default use the thresholds configured in the glances.conf file (see #2058)
- alert = self.get_alert(current=i['value'], header='temperature_' + i['label'])
+ alert = self.get_alert(current=i['value'], header=SensorType.CPU_TEMP + '_' + i['label'])
else:
# Else use the system thresholds
if i['critical'] is None:
@@ -222,7 +239,8 @@ class PluginModel(GlancesPluginModel):
alert = 'WARNING'
else:
alert = 'OK'
- elif i['type'] == 'battery':
+ elif i['type'] == SensorType.BATTERY:
+ # Battery is in %
alert = self.get_alert(current=100 - i['value'], header=i['type'])
else:
alert = self.get_alert(current=i['value'], header=i['type'])
@@ -265,7 +283,7 @@ class PluginModel(GlancesPluginModel):
# Stats
for i in self.stats:
# Do not display anything if no battery are detected
- if i['type'] == 'battery' and i['value'] == []:
+ if i['type'] == SensorType.BATTERY and i['value'] == []:
continue
# New line
ret.append(self.curse_new_line())
@@ -277,7 +295,7 @@ class PluginModel(GlancesPluginModel):
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration'))
)
else:
- if args.fahrenheit and i['type'] != 'battery' and i['type'] != SENSOR_FAN_TYPE:
+ if args.fahrenheit and i['type'] != SensorType.BATTERY and i['type'] != SensorType.FAN_SPEED:
trend = ''
value = to_fahrenheit(i['value'])
unit = 'F'
@@ -302,73 +320,42 @@ class PluginModel(GlancesPluginModel):
class GlancesGrabSensors(object):
"""Get sensors stats."""
- def __init__(self):
+ def __init__(self, sensor_type: Literal[SensorType.FAN_SPEED, SensorType.CPU_TEMP]):
"""Init sensors stats."""
- # Temperatures
- self.init_temp = False
- self.sensor_temps = {}
+ self.sensor_type = sensor_type
+ self.sensor_unit = CPU_TEMP_UNIT if self.sensor_type == SensorType.CPU_TEMP else FAN_SPEED_UNIT
+
+ self.init = False
try:
- # psutil>=5.1.0, Linux-only
- self.sensor_temps = psutil.sensors_temperatures()
+ self.__fetch_psutil()
+ self.init = True
except AttributeError:
- logger.debug("Cannot grab temperatures. Platform not supported.")
- else:
- self.init_temp = True
+ logger.debug(f"Cannot grab {sensor_type}. Platform not supported.")
+
+ def __fetch_psutil(self) -> Dict[str, list]:
+ if self.sensor_type == SensorType.CPU_TEMP:
# Solve an issue #1203 concerning a RunTimeError warning message displayed
# in the curses interface.
warnings.filterwarnings("ignore")
- # Fans
- self.init_fan = False
- self.sensor_fans = {}
- try:
- # psutil>=5.2.0, Linux-only
- self.sensor_fans = psutil.sensors_fans()
- except AttributeError:
- logger.debug("Cannot grab fans speed. Platform not supported.")
- else:
- self.init_fan = True
+ # psutil>=5.1.0, Linux-only
+ return psutil.sensors_temperatures()
- # Init the stats
- self.reset()
+ if self.sensor_type == SensorType.FAN_SPEED:
+ # psutil>=5.2.0, Linux-only
+ return psutil.sensors_fans()
- def reset(self):
- """Reset/init the stats."""
- self.sensors_list = []
+ raise ValueError(f"Unsupported sensor_type: {self.sensor_type}")
- def __update__(self):
+ def update(self) -> List[dict]:
"""Update the stats."""
- # Reset the list
- self.reset()
-
- if not self.init_temp:
- return self.sensors_list
+ if not self.init:
+ return []
# Temperatures sensors
- self.sensors_list.extend(self.build_sensors_list(SENSOR_TEMP_UNIT))
-
- # Fans sensors
- self.sensors_list.extend(self.build_sensors_list(SENSOR_FAN_UNIT))
-
- return self.sensors_list
-
- def build_sensors_list(self, type):
- """Build the sensors list depending of the type.
-
- type: SENSOR_TEMP_UNIT or SENSOR_FAN_UNIT
-
- output: a list
- """
ret = []
- if type == SENSOR_TEMP_UNIT and self.init_temp:
- input_list = self.sensor_temps
- self.sensor_temps = psutil.sensors_temperatures()
- elif type == SENSOR_FAN_UNIT and self.init_fan:
- input_list = self.sensor_fans
- self.sensor_fans = psutil.sensors_fans()
- else:
- return ret
- for chip_name, chip in iteritems(input_list):
+ data = self.__fetch_psutil()
+ for chip_name, chip in data.items():
label_index = 1
for chip_name_index, feature in enumerate(chip):
sensors_current = {}
@@ -381,8 +368,9 @@ class GlancesGrabSensors(object):
else:
sensors_current['label'] = feature.label
# Sensors value, limit and unit
- sensors_current['unit'] = type
- sensors_current['value'] = int(getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
+ sensors_current['unit'] = self.sensor_unit
+ sensors_current['value'] = int(
+ getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
system_warning = getattr(feature, 'high', None)
system_critical = getattr(feature, 'critical', None)
sensors_current['warning'] = int(system_warning) if system_warning is not None else None
@@ -390,16 +378,3 @@ class GlancesGrabSensors(object):
# Add sensor to the list
ret.append(sensors_current)
return ret
-
- def get(self, sensor_type=SENSOR_TEMP_TYPE):
- """Get sensors list."""
- self.__update__()
- if sensor_type == SENSOR_TEMP_TYPE:
- ret = [s for s in self.sensors_list if s['unit'] == SENSOR_TEMP_UNIT]
- elif sensor_type == SENSOR_FAN_TYPE:
- ret = [s for s in self.sensors_list if s['unit'] == SENSOR_FAN_UNIT]
- else:
- # Unknown type
- logger.debug("Unknown sensor type %s" % sensor_type)
- ret = []
- return ret