From 7a25f7165e7cb88e4e7b8577fa6f32d571b03e4c Mon Sep 17 00:00:00 2001 From: Bharath Vignesh J K <52282402+RazCrimson@users.noreply.github.com> Date: Tue, 14 May 2024 13:37:20 +0530 Subject: [PATCH] refactor: plugin(sensors) - cleanup + typing + fixes --- glances/plugins/sensors/__init__.py | 221 +++++++++++----------------- 1 file changed, 82 insertions(+), 139 deletions(-) diff --git a/glances/plugins/sensors/__init__.py b/glances/plugins/sensors/__init__.py index 0173235a..1815de84 100644 --- a/glances/plugins/sensors/__init__.py +++ b/glances/plugins/sensors/__init__.py @@ -8,30 +8,33 @@ # """Sensors plugin.""" +from enum import Enum +from concurrent.futures import ThreadPoolExecutor +from typing import List, Dict, Literal, Any import psutil import warnings -import threading 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' -SENSOR_HDDTEMP_TYPE = 'temperature_hdd' -SENSOR_HDDTEMP_UNIT = 'C' -SENSORS_BATTERY_TYPE = 'battery' -SENSORS_BATTERY_UNIT = '%' +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 @@ -82,28 +85,38 @@ class PluginModel(GlancesPluginModel): super(PluginModel, self).__init__( args=args, config=config, stats_init_value=[], fields_description=fields_description ) - start_duration = Counter() # Init the sensor class start_duration.reset() - # Hotfix! Refactor to use only one `GlancesGrabSensors` later - self.glances_grab_sensors_fan_speed = GlancesGrabSensors() - self.glances_grab_sensors_temperature = 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())) - # Instance for the HDDTemp Plugin in order to display the hard disks - # temperatures start_duration.reset() - self.hddtemp_plugin = HddTempPluginModel(args=args, config=config) + 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 + start_duration.reset() + 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 @@ -115,37 +128,15 @@ class PluginModel(GlancesPluginModel): """Return the key of the list.""" return 'label' - def __get_temperature(self, stats, index): + def __get_sensor_data(self, sensor_type: SensorType) -> List[Dict]: try: - temperature = self.__set_type(self.glances_grab_sensors_temperature.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE) + data = self.sensors_grab_map[sensor_type].update() + data = self.__set_type(data, sensor_type) except Exception as e: - logger.error("Cannot grab sensors temperatures (%s)" % e) + logger.error(f"Cannot grab sensors `{sensor_type}` ({e})") + return [] else: - stats[index] = self.__transform_sensors(temperature) - - def __get_fan_speed(self, stats, index): - try: - fan_speed = self.__set_type(self.glances_grab_sensors_fan_speed.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE) - except Exception as e: - logger.error("Cannot grab FAN speed (%s)" % e) - else: - stats[index] = self.__transform_sensors(fan_speed) - - def __get_hddtemp(self, stats, index): - try: - hddtemp = self.__set_type(self.hddtemp_plugin.update(), SENSOR_HDDTEMP_TYPE) - except Exception as e: - logger.error("Cannot grab HDD temperature (%s)" % e) - else: - stats[index] = self.__transform_sensors(hddtemp) - - def __get_bat_percent(self, stats, index): - try: - bat_percent = self.__set_type(self.batpercent_plugin.update(), SENSORS_BATTERY_TYPE) - except Exception as e: - logger.error("Cannot grab battery percent (%s)" % e) - else: - stats[index] = self.__transform_sensors(bat_percent) + return self.__transform_sensors(data) def __transform_sensors(self, threads_stats): """Hide, alias and sort the result""" @@ -172,22 +163,17 @@ class PluginModel(GlancesPluginModel): stats = self.get_init_value() if self.input_method == 'local': - threads_stats = [None] * 4 - threads = [ - threading.Thread(name=SENSOR_TEMP_TYPE, target=self.__get_temperature, args=(threads_stats, 0)), - threading.Thread(name=SENSOR_FAN_TYPE, target=self.__get_fan_speed, args=(threads_stats, 1)), - threading.Thread(name=SENSOR_HDDTEMP_TYPE, target=self.__get_hddtemp, args=(threads_stats, 2)), - threading.Thread(name=SENSORS_BATTERY_TYPE, target=self.__get_bat_percent, args=(threads_stats, 3)), - ] - # Start threads in // - for t in threads: - t.start() - # Wait threads are finished - for t in threads: - t.join() + 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 s in threads_stats: - stats.extend(s) + 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 # No standard: @@ -220,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()}) @@ -237,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=SENSOR_TEMP_TYPE + '_' + 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=SENSOR_TEMP_TYPE + '_' + 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: @@ -253,7 +239,7 @@ class PluginModel(GlancesPluginModel): alert = 'WARNING' else: alert = 'OK' - elif i['type'] == SENSORS_BATTERY_TYPE: + elif i['type'] == SensorType.BATTERY: # Battery is in % alert = self.get_alert(current=100 - i['value'], header=i['type']) else: @@ -297,7 +283,7 @@ class PluginModel(GlancesPluginModel): # Stats for i in self.stats: # Do not display anything if no battery are detected - if i['type'] == SENSORS_BATTERY_TYPE and i['value'] == []: + if i['type'] == SensorType.BATTERY and i['value'] == []: continue # New line ret.append(self.curse_new_line()) @@ -309,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'] != SENSORS_BATTERY_TYPE 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' @@ -334,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.1.0, Linux-only + return psutil.sensors_temperatures() + + if self.sensor_type == SensorType.FAN_SPEED: # 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 + return psutil.sensors_fans() - # Init the stats - self.reset() + raise ValueError(f"Unsupported sensor_type: {self.sensor_type}") - def reset(self): - """Reset/init the stats.""" - self.sensors_list = [] - - 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 = {} @@ -413,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 @@ -422,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