From c870b39589d37fa8325b1e99bd5ad2712f656179 Mon Sep 17 00:00:00 2001 From: Arun Saravanan Balachandran Date: Mon, 19 Aug 2019 09:35:30 -0400 Subject: [PATCH] DellEMC S6100 : Platform2.0 API implementation [Module, Thermal] --- .../s6100/sonic_platform/__init__.py | 2 +- .../s6100/sonic_platform/chassis.py | 77 ++-------- .../s6100/sonic_platform/eeprom.py | 38 +++++ .../s6100/sonic_platform/fan.py | 6 +- .../s6100/sonic_platform/module.py | 136 +++++++++++++++++ .../s6100/sonic_platform/psu.py | 34 +++-- .../s6100/sonic_platform/thermal.py | 143 ++++++++++++++++++ 7 files changed, 357 insertions(+), 79 deletions(-) create mode 100644 platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/module.py create mode 100644 platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/thermal.py diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/__init__.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/__init__.py index 61b486d775..96de5a93c4 100755 --- a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/__init__.py +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/__init__.py @@ -1,3 +1,3 @@ -__all__ = ["platform", "chassis", "fan", "psu", "sfp"] +__all__ = ["platform", "chassis", "module", "fan", "psu", "sfp", "thermal"] from sonic_platform import * diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/chassis.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/chassis.py index 53d4eadaec..8b0252b32a 100755 --- a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/chassis.py +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/chassis.py @@ -14,12 +14,16 @@ try: from sonic_platform.sfp import Sfp from sonic_platform.psu import Psu from sonic_platform.fan import Fan + from sonic_platform.module import Module + from sonic_platform.thermal import Thermal from eeprom import Eeprom except ImportError as e: raise ImportError(str(e) + "- required module not found") +MAX_S6100_MODULE = 4 MAX_S6100_FAN = 4 MAX_S6100_PSU = 2 +MAX_S6100_THERMAL = 10 class Chassis(ChassisBase): @@ -31,39 +35,6 @@ class Chassis(ChassisBase): HWMON_NODE = os.listdir(HWMON_DIR)[0] MAILBOX_DIR = HWMON_DIR + HWMON_NODE - PORT_START = 0 - PORT_END = 63 - PORTS_IN_BLOCK = (PORT_END + 1) - IOM1_PORT_START = 0 - IOM2_PORT_START = 16 - IOM3_PORT_START = 32 - IOM4_PORT_START = 48 - - PORT_I2C_MAPPING = {} - # 0th Index = i2cLine, 1st Index = EepromIdx in i2cLine - EEPROM_I2C_MAPPING = { - # IOM 1 - 0: [6, 66], 1: [6, 67], 2: [6, 68], 3: [6, 69], - 4: [6, 70], 5: [6, 71], 6: [6, 72], 7: [6, 73], - 8: [6, 74], 9: [6, 75], 10: [6, 76], 11: [6, 77], - 12: [6, 78], 13: [6, 79], 14: [6, 80], 15: [6, 81], - # IOM 2 - 16: [8, 50], 17: [8, 51], 18: [8, 52], 19: [8, 53], - 20: [8, 54], 21: [8, 55], 22: [8, 56], 23: [8, 57], - 24: [8, 58], 25: [8, 59], 26: [8, 60], 27: [8, 61], - 28: [8, 62], 29: [8, 63], 30: [8, 64], 31: [8, 65], - # IOM 3 - 32: [7, 34], 33: [7, 35], 34: [7, 36], 35: [7, 37], - 36: [7, 38], 37: [7, 39], 38: [7, 40], 39: [7, 41], - 40: [7, 42], 41: [7, 43], 42: [7, 44], 43: [7, 45], - 44: [7, 46], 45: [7, 47], 46: [7, 48], 47: [7, 49], - # IOM 4 - 48: [9, 18], 49: [9, 19], 50: [9, 20], 51: [9, 21], - 52: [9, 22], 53: [9, 23], 54: [9, 24], 55: [9, 25], - 56: [9, 26], 57: [9, 27], 58: [9, 28], 59: [9, 29], - 60: [9, 30], 61: [9, 31], 62: [9, 32], 63: [9, 33] - } - reset_reason_dict = {} reset_reason_dict[11] = ChassisBase.REBOOT_CAUSE_POWER_LOSS reset_reason_dict[33] = ChassisBase.REBOOT_CAUSE_WATCHDOG @@ -81,6 +52,10 @@ class Chassis(ChassisBase): ChassisBase.__init__(self) # Initialize EEPROM self.sys_eeprom = Eeprom() + for i in range(MAX_S6100_MODULE): + module = Module(i) + self._module_list.append(module) + for i in range(MAX_S6100_FAN): fan = Fan(i) self._fan_list.append(fan) @@ -89,38 +64,9 @@ class Chassis(ChassisBase): psu = Psu(i) self._psu_list.append(psu) - self._populate_port_i2c_mapping() - - # sfp.py will read eeprom contents and retrive the eeprom data. - # It will also provide support sfp controls like reset and setting - # low power mode. - # We pass the eeprom path and sfp control path from chassis.py - # So that sfp.py implementation can be generic to all platforms - eeprom_base = "/sys/class/i2c-adapter/i2c-{0}/i2c-{1}/{1}-0050/eeprom" - sfp_ctrl_base = "/sys/class/i2c-adapter/i2c-{0}/{0}-003e/" - for index in range(0, self.PORTS_IN_BLOCK): - eeprom_path = eeprom_base.format(self.EEPROM_I2C_MAPPING[index][0], - self.EEPROM_I2C_MAPPING[index][1]) - sfp_control = sfp_ctrl_base.format(self.PORT_I2C_MAPPING[index]) - sfp_node = Sfp(index, 'QSFP', eeprom_path, sfp_control, index) - self._sfp_list.append(sfp_node) - - def _populate_port_i2c_mapping(self): - # port_num and i2c match - for port_num in range(0, self.PORTS_IN_BLOCK): - if((port_num >= self.IOM1_PORT_START) and - (port_num < self.IOM2_PORT_START)): - i2c_line = 14 - elif((port_num >= self.IOM2_PORT_START) and - (port_num < self.IOM3_PORT_START)): - i2c_line = 16 - elif((port_num >= self.IOM3_PORT_START) and - (port_num = self.IOM4_PORT_START) and - (port_num < self.PORTS_IN_BLOCK)): - i2c_line = 17 - self.PORT_I2C_MAPPING[port_num] = i2c_line + for i in range(MAX_S6100_THERMAL): + thermal = Thermal(i) + self._thermal_list.append(thermal) def _get_pmc_register(self, reg_name): # On successful read, returns the value read from given @@ -200,6 +146,7 @@ class Chassis(ChassisBase): OCP ONIE TlvInfo EEPROM format and values are their corresponding values. """ + return self.sys_eeprom.system_eeprom_info() def get_reboot_cause(self): """ diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/eeprom.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/eeprom.py index 86c314f3ef..4e683e1e51 100644 --- a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/eeprom.py +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/eeprom.py @@ -21,11 +21,42 @@ class Eeprom(eeprom_tlvinfo.TlvInfoDecoder): def __init__(self): self.eeprom_path = "/sys/class/i2c-adapter/i2c-2/2-0050/eeprom" super(Eeprom, self).__init__(self.eeprom_path, 0, '', True) + self.eeprom_tlv_dict = dict() try: self.eeprom_data = self.read_eeprom() except: self.eeprom_data = "N/A" raise RuntimeError("Eeprom is not Programmed") + else: + eeprom = self.eeprom_data + + if not self.is_valid_tlvinfo_header(eeprom): + return + + total_length = (ord(eeprom[9]) << 8) | ord(eeprom[10]) + tlv_index = self._TLV_INFO_HDR_LEN + tlv_end = self._TLV_INFO_HDR_LEN + total_length + + while (tlv_index + 2) < len(eeprom) and tlv_index < tlv_end: + if not self.is_valid_tlv(eeprom[tlv_index:]): + break + + tlv = eeprom[tlv_index:tlv_index + 2 + + ord(eeprom[tlv_index + 1])] + code = "0x%02X" % (ord(tlv[0])) + + if ord(tlv[0]) == self._TLV_CODE_VENDOR_EXT: + value = str((ord(tlv[2]) << 24) | (ord(tlv[3]) << 16) | + (ord(tlv[4]) << 8) | ord(tlv[5])) + value += str(tlv[6:6 + ord(tlv[1])]) + else: + name, value = self.decoder(None, tlv) + + self.eeprom_tlv_dict[code] = value + if ord(eeprom[tlv_index]) == self._TLV_CODE_CRC_32: + break + + tlv_index += ord(eeprom[tlv_index+1]) + 2 def serial_number_str(self): @@ -76,3 +107,10 @@ class Eeprom(eeprom_tlvinfo.TlvInfoDecoder): return results[2] + def system_eeprom_info(self): + """ + Returns a dictionary, where keys are the type code defined in + ONIE EEPROM format and values are their corresponding values + found in the system EEPROM. + """ + return self.eeprom_tlv_dict diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/fan.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/fan.py index c822ccdae2..d84f8feb9b 100644 --- a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/fan.py +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/fan.py @@ -26,7 +26,7 @@ class Fan(FanBase): HWMON_NODE = os.listdir(HWMON_DIR)[0] MAILBOX_DIR = HWMON_DIR + HWMON_NODE - def __init__(self, fantray_index, fan_index=1, psu_fan=False): + def __init__(self, fantray_index=1, fan_index=1, psu_fan=False): self.is_psu_fan = psu_fan if not self.is_psu_fan: # API index is starting from 0, DellEMC platform index is starting @@ -74,7 +74,7 @@ class Fan(FanBase): return "FanTray{}-Fan{}".format( self.fantrayindex, self.fanindex - 1) else: - return "PSU{} Fan".format(self.index - 10) + return "PSU{} Fan".format(self.fanindex - 10) def get_model(self): """ @@ -209,7 +209,7 @@ class Fan(FanBase): status = False return status - def get_target_speed(self): + def get_target_speed(self): """ Retrieves the target (expected) speed of the fan Returns: diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/module.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/module.py new file mode 100644 index 0000000000..9d2c745b01 --- /dev/null +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/module.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +######################################################################## +# DellEMC S6100 +# +# Module contains an implementation of SONiC Platform Base API and +# provides the Modules' information which are available in the platform +# +######################################################################## + + +try: + import os + from sonic_platform_base.module_base import ModuleBase + from sonic_platform.sfp import Sfp +except ImportError as e: + raise ImportError(str(e) + "- required module not found") + + +class Module(ModuleBase): + """DellEMC Platform-specific Module class""" + + HWMON_DIR = "/sys/devices/platform/SMF.512/hwmon/" + HWMON_NODE = os.listdir(HWMON_DIR)[0] + MAILBOX_DIR = HWMON_DIR + HWMON_NODE + + IOM_I2C_MAPPING = { 1: 14, 2: 16, 3: 15, 4: 17 } + EEPROM_I2C_MAPPING = { + # IOM 1 + 0: [6, 66], 1: [6, 67], 2: [6, 68], 3: [6, 69], + 4: [6, 70], 5: [6, 71], 6: [6, 72], 7: [6, 73], + 8: [6, 74], 9: [6, 75], 10: [6, 76], 11: [6, 77], + 12: [6, 78], 13: [6, 79], 14: [6, 80], 15: [6, 81], + # IOM 2 + 16: [8, 50], 17: [8, 51], 18: [8, 52], 19: [8, 53], + 20: [8, 54], 21: [8, 55], 22: [8, 56], 23: [8, 57], + 24: [8, 58], 25: [8, 59], 26: [8, 60], 27: [8, 61], + 28: [8, 62], 29: [8, 63], 30: [8, 64], 31: [8, 65], + # IOM 3 + 32: [7, 34], 33: [7, 35], 34: [7, 36], 35: [7, 37], + 36: [7, 38], 37: [7, 39], 38: [7, 40], 39: [7, 41], + 40: [7, 42], 41: [7, 43], 42: [7, 44], 43: [7, 45], + 44: [7, 46], 45: [7, 47], 46: [7, 48], 47: [7, 49], + # IOM 4 + 48: [9, 18], 49: [9, 19], 50: [9, 20], 51: [9, 21], + 52: [9, 22], 53: [9, 23], 54: [9, 24], 55: [9, 25], + 56: [9, 26], 57: [9, 27], 58: [9, 28], 59: [9, 29], + 60: [9, 30], 61: [9, 31], 62: [9, 32], 63: [9, 33] + } + + def __init__(self, module_index): + # Modules are 1-based in DellEMC platforms + self.index = module_index + 1 + self.port_start = (self.index - 1) * 16 + self.port_end = (self.index * 16) - 1 + self.port_i2c_line = self.IOM_I2C_MAPPING[self.index] + + self.iom_status_reg = "iom_status" + self.iom_presence_reg = "iom_presence" + + # Overriding _sfp_list class variable defined in ModuleBase, to + # make it unique per Module object + self._sfp_list = [] + + eeprom_base = "/sys/class/i2c-adapter/i2c-{0}/i2c-{1}/{1}-0050/eeprom" + sfp_ctrl_base = "/sys/class/i2c-adapter/i2c-{0}/{0}-003e/" + + # sfp.py will read eeprom contents and retrive the eeprom data. + # It will also provide support sfp controls like reset and setting + # low power mode. + for index in range(self.port_start, self.port_end + 1): + eeprom_path = eeprom_base.format(self.EEPROM_I2C_MAPPING[index][0], + self.EEPROM_I2C_MAPPING[index][1]) + sfp_control = sfp_ctrl_base.format(self.port_i2c_line) + sfp_node = Sfp(index, 'QSFP', eeprom_path, sfp_control, index) + self._sfp_list.append(sfp_node) + + def _get_pmc_register(self, reg_name): + # On successful read, returns the value read from given + # reg_name and on failure returns 'ERR' + rv = 'ERR' + mb_reg_file = self.MAILBOX_DIR + '/' + reg_name + + if (not os.path.isfile(mb_reg_file)): + return rv + + try: + with open(mb_reg_file, 'r') as fd: + rv = fd.read() + except Exception as error: + rv = 'ERR' + + rv = rv.rstrip('\r\n') + rv = rv.lstrip(" ") + return rv + + def get_name(self): + """ + Retrieves the name of the device + + Returns: + string: The name of the device + """ + return "IOM{}: 16xQSFP+".format(self.index) + + def get_presence(self): + """ + Retrieves the presence of the Module + + Returns: + bool: True if Module is present, False if not + """ + status = False + iom_presence = self._get_pmc_register(self.iom_presence_reg) + if (iom_presence != 'ERR'): + iom_presence = int(iom_presence,16) + if (~iom_presence & (1 << (self.index - 1))): + status = True + + return status + + def get_status(self): + """ + Retrieves the operational status of the Module + + Returns: + bool: True if Module is operating properly, False if not + """ + status = False + iom_status = self._get_pmc_register(self.iom_status_reg) + if (iom_status != 'ERR'): + iom_status = int(iom_status,16) + if (~iom_status & (1 << (self.index - 1))): + status = True + + return status diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/psu.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/psu.py index 39ccf1b655..960e3b4cea 100644 --- a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/psu.py +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/psu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python ######################################################################## -# DellEMC +# DellEMC S6100 # # Module contains an implementation of SONiC Platform Base API and # provides the PSUs' information which are available in the platform @@ -43,10 +43,10 @@ class Psu(PsuBase): self._fan_list = [] # Passing True to specify it is a PSU fan - psu_fan = Fan(self.index, True) + psu_fan = Fan(fan_index=self.index, psu_fan=True) self._fan_list.append(psu_fan) - def get_pmc_register(self, reg_name): + def _get_pmc_register(self, reg_name): # On successful read, returns the value read from given # reg_name and on failure returns 'ERR' rv = 'ERR' @@ -82,7 +82,7 @@ class Psu(PsuBase): bool: True if PSU is present, False if not """ status = False - psu_presence = self.get_pmc_register(self.psu_presence_reg) + psu_presence = self._get_pmc_register(self.psu_presence_reg) if (psu_presence != 'ERR'): psu_presence = int(psu_presence, 16) # Checking whether bit 0 is not set @@ -100,7 +100,7 @@ class Psu(PsuBase): """ # For Serial number "US-01234D-54321-25A-0123-A00", the part # number is "01234D" - psu_serialno = self.get_pmc_register(self.psu_serialno_reg) + psu_serialno = self._get_pmc_register(self.psu_serialno_reg) if (psu_serialno != 'ERR') and self.get_presence(): if (len(psu_serialno.split('-')) > 1): psu_partno = psu_serialno.split('-')[1] @@ -119,7 +119,7 @@ class Psu(PsuBase): string: Serial number of PSU """ # Sample Serial number format "US-01234D-54321-25A-0123-A00" - psu_serialno = self.get_pmc_register(self.psu_serialno_reg) + psu_serialno = self._get_pmc_register(self.psu_serialno_reg) if (psu_serialno == 'ERR') or not self.get_presence(): psu_serialno = 'NA' @@ -133,7 +133,7 @@ class Psu(PsuBase): bool: True if PSU is operating properly, False if not """ status = False - psu_status = self.get_pmc_register(self.psu_presence_reg) + psu_status = self._get_pmc_register(self.psu_presence_reg) if (psu_status != 'ERR'): psu_status = int(psu_status, 16) # Checking whether both bit 3 and bit 2 are not set @@ -150,7 +150,7 @@ class Psu(PsuBase): A float number, the output voltage in volts, e.g. 12.1 """ - psu_voltage = self.get_pmc_register(self.psu_voltage_reg) + psu_voltage = self._get_pmc_register(self.psu_voltage_reg) if (psu_voltage != 'ERR') and self.get_presence(): # Converting the value returned by driver which is in # millivolts to volts @@ -168,7 +168,7 @@ class Psu(PsuBase): A float number, electric current in amperes, e.g. 15.4 """ - psu_current = self.get_pmc_register(self.psu_current_reg) + psu_current = self._get_pmc_register(self.psu_current_reg) if (psu_current != 'ERR') and self.get_presence(): # Converting the value returned by driver which is in # milliamperes to amperes @@ -186,7 +186,7 @@ class Psu(PsuBase): A float number, the power in watts, e.g. 302.6 """ - psu_power = self.get_pmc_register(self.psu_power_reg) + psu_power = self._get_pmc_register(self.psu_power_reg) if (psu_power != 'ERR') and self.get_presence(): # Converting the value returned by driver which is in # microwatts to watts @@ -196,6 +196,20 @@ class Psu(PsuBase): return psu_power + def get_powergood_status(self): + """ + Retrieves the powergood status of PSU + + Returns: + A boolean, True if PSU has stablized its output voltages and + passed all its internal self-tests, False if not. + """ + status = False + if self.get_status() and self._fan_list[0].get_status(): + status = True + + return status + def set_status_led(self): """ Sets the state of the PSU status LED diff --git a/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/thermal.py b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/thermal.py new file mode 100644 index 0000000000..5128111d5f --- /dev/null +++ b/platform/broadcom/sonic-platform-modules-dell/s6100/sonic_platform/thermal.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python + +######################################################################## +# DellEMC S6100 +# +# Module contains an implementation of SONiC Platform Base API and +# provides the Thermals' information which are available in the platform +# +######################################################################## + + +try: + import os + from sonic_platform_base.thermal_base import ThermalBase +except ImportError as e: + raise ImportError(str(e) + "- required module not found") + + +class Thermal(ThermalBase): + """DellEMC Platform-specific Thermal class""" + + THERMAL_NAME = ( + 'CPU On-board', 'ASIC On-board Front', 'System Front', + 'ASIC On-board Rear', 'Front GE board', 'Front SFP+ board', + 'CPU Core 0', 'CPU Core 1', 'CPU Core 2', 'CPU Core 3' + ) + + def __init__(self, thermal_index): + self.is_cpu_thermal = False + self.index = thermal_index + 1 + + if self.index < 7: + if self.index < 5: + hwmon_temp_index = self.index + else: + hwmon_temp_index = self.index + 5 + + dev_path = "/sys/devices/platform/SMF.512/hwmon/" + else: + hwmon_temp_index = self.index - 5 + self.is_cpu_thermal = True + dev_path = "/sys/devices/platform/coretemp.0/hwmon/" + + hwmon_node = os.listdir(dev_path)[0] + self.HWMON_DIR = dev_path + hwmon_node + '/' + + self.thermal_status_file = self.HWMON_DIR \ + + "temp{}_alarm".format(hwmon_temp_index) + self.thermal_temperature_file = self.HWMON_DIR \ + + "temp{}_input".format(hwmon_temp_index) + self.thermal_high_threshold_file = self.HWMON_DIR \ + + "temp{}_crit".format(hwmon_temp_index) + + def _read_sysfs_file(self, sysfs_file): + # On successful read, returns the value read from given + # sysfs_file and on failure returns 'ERR' + rv = 'ERR' + + if (not os.path.isfile(sysfs_file)): + return rv + + try: + with open(sysfs_file, 'r') as fd: + rv = fd.read() + except Exception as error: + rv = 'ERR' + + rv = rv.rstrip('\r\n') + rv = rv.lstrip(" ") + return rv + + def get_name(self): + """ + Retrieves the name of the thermal + + Returns: + string: The name of the thermal + """ + return self.THERMAL_NAME[self.index - 1] + + def get_presence(self): + """ + Retrieves the presence of the thermal + + Returns: + bool: True if thermal is present, False if not + """ + return True + + def get_status(self): + """ + Retrieves the operational status of the thermal + + Returns: + A boolean value, True if thermal is operating properly, + False if not + """ + status = False + if self.is_cpu_thermal: + status = True + else: + thermal_status = self._read_sysfs_file(self.thermal_status_file) + if (thermal_status != 'ERR'): + thermal_status = int(thermal_status, 16) + if thermal_status != 5: + status = True + + return status + + def get_temperature(self): + """ + Retrieves current temperature reading from thermal + + Returns: + A float number of current temperature in Celsius up to + nearest thousandth of one degree Celsius, e.g. 30.125 + """ + thermal_temperature = self._read_sysfs_file( + self.thermal_temperature_file) + if (thermal_temperature != 'ERR'): + thermal_temperature = float(thermal_temperature) / 1000 + else: + thermal_temperature = 0 + + return "{:.3f}".format(thermal_temperature) + + def get_high_threshold(self): + """ + Retrieves the high threshold temperature of thermal + + Returns: + A float number, the high threshold temperature of thermal in + Celsius up to nearest thousandth of one degree Celsius, + e.g. 30.125 + """ + thermal_high_threshold = self._read_sysfs_file( + self.thermal_high_threshold_file) + if (thermal_high_threshold != 'ERR'): + thermal_high_threshold = float(thermal_high_threshold) / 1000 + else: + thermal_high_threshold = 0 + + return "{:.3f}".format(thermal_high_threshold)