55"""
66import argparse
77import json
8+ from datetime import datetime
89
910from tabulate import tabulate
1011from swsscommon .swsscommon import SonicV2Connector
1112from natsort import natsorted
1213
14+ try :
15+ from sonic_platform_base .sonic_sfp .sfputilhelper import SfpUtilHelper
16+ except ImportError :
17+ SfpUtilHelper = None
18+
19+ try :
20+ from sonic_py_common import device_info , multi_asic
21+ except ImportError :
22+ device_info = None
23+ multi_asic = None
24+
1325
1426header = ['Sensor' , 'Temperature' , 'High TH' , 'Low TH' , 'Crit High TH' , 'Crit Low TH' , 'Warning' , 'Timestamp' ]
1527
@@ -22,59 +34,186 @@ CRIT_HIGH_THRESH_FIELD_NAME = 'critical_high_threshold'
2234CRIT_LOW_THRESH_FIELD_NAME = 'critical_low_threshold'
2335WARNING_STATUS_FIELD_NAME = 'warning_status'
2436
37+ # SFP/transceiver temperature is published by xcvrd (not thermalctld) into
38+ # the dedicated TRANSCEIVER_DOM_TEMPERATURE table; thresholds come from
39+ # TRANSCEIVER_DOM_THRESHOLD; per-flag warning/alarm state comes from
40+ # TRANSCEIVER_DOM_FLAG. Field names are defined by xcvrd.
41+ SFP_TEMPER_TABLE_NAME = 'TRANSCEIVER_DOM_TEMPERATURE'
42+ SFP_THRESHOLD_TABLE_NAME = 'TRANSCEIVER_DOM_THRESHOLD'
43+ SFP_FLAG_TABLE_NAME = 'TRANSCEIVER_DOM_FLAG'
44+ SFP_DISPLAY_TIME_FORMAT = '%Y%m%d %H:%M:%S'
45+ SFP_TEMPER_FIELD_NAME = 'temperature'
46+ SFP_TEMP_HIGH_WARN_FIELD = 'temphighwarning'
47+ SFP_TEMP_LOW_WARN_FIELD = 'templowwarning'
48+ SFP_TEMP_HIGH_ALARM_FIELD = 'temphighalarm'
49+ SFP_TEMP_LOW_ALARM_FIELD = 'templowalarm'
50+ SFP_TEMP_FLAG_FIELDS = (
51+ SFP_TEMP_HIGH_WARN_FIELD ,
52+ SFP_TEMP_LOW_WARN_FIELD ,
53+ SFP_TEMP_HIGH_ALARM_FIELD ,
54+ SFP_TEMP_LOW_ALARM_FIELD ,
55+ )
56+
57+ NA = 'N/A'
58+
2559
2660class TemperShow (object ):
2761 def __init__ (self ):
2862 self .db = SonicV2Connector (host = "127.0.0.1" )
2963 self .db .connect (self .db .STATE_DB )
64+ self .sfp_util = self ._init_sfp_util_helper ()
3065
31- def show (self , output_json ):
32- keys = self .db .keys (self .db .STATE_DB , TEMPER_TABLE_NAME + '*' )
33- if not keys :
34- print ('Thermal Not detected\n ' )
35- return
66+ def _init_sfp_util_helper (self ):
67+ """Initialize SfpUtilHelper for logical-port -> physical-port mapping.
3668
37- table = []
38- json_output = []
69+ Returns None if the helper or platform info is unavailable; SFP rows
70+ will then fall back to displaying the logical port name.
71+ """
72+ if SfpUtilHelper is None or device_info is None or multi_asic is None :
73+ return None
74+ try :
75+ sfp_util = SfpUtilHelper ()
76+ if multi_asic .is_multi_asic ():
77+ _ , hwsku_path = device_info .get_paths_to_platform_and_hwsku_dirs ()
78+ sfp_util .read_all_porttab_mappings (hwsku_path , multi_asic .get_num_asics ())
79+ else :
80+ port_config_file_path = device_info .get_path_to_port_config_file ()
81+ sfp_util .read_porttab_mappings (port_config_file_path , 0 )
82+ return sfp_util
83+ except Exception :
84+ return None
85+
86+ def _sfp_display_name (self , port_name ):
87+ """Map a logical port name (e.g. 'Ethernet0') to 'xSFP module <N> Temp'.
88+
89+ Falls back to the logical port name if the mapping is unavailable.
90+ This matches the legacy thermalctld naming convention so existing
91+ consumers/operators see the same sensor labels.
92+ """
93+ if self .sfp_util is None :
94+ return port_name
95+ try :
96+ physical = self .sfp_util .get_logical_to_physical (port_name )
97+ if physical and len (physical ) > 0 and physical [0 ] is not None :
98+ # physical port index from SfpUtilHelper is already 1-based
99+ return 'xSFP module {} Temp' .format (physical [0 ])
100+ except Exception :
101+ pass
102+ return port_name
103+
104+ def _collect_platform_sensors (self ):
105+ """Collect chassis/PSU/fan thermal sensors from TEMPERATURE_INFO."""
106+ rows = []
107+ keys = self .db .keys (self .db .STATE_DB , TEMPER_TABLE_NAME + '|*' )
108+ if not keys :
109+ return rows
39110 for key in natsorted (keys ):
40111 key_list = key .split ('|' )
41- if len (key_list ) != 2 : # error data in DB, log it and ignore
112+ if len (key_list ) != 2 :
42113 print ('Warn: Invalid key in table {}: {}' .format (TEMPER_TABLE_NAME , key ))
43114 continue
44-
45115 name = key_list [1 ]
46- data_dict = self .db .get_all (self .db .STATE_DB , key )
47- if output_json :
48- json_output .append ({
49- "Sensor" : name ,
50- "Temperature" : data_dict [TEMPER_FIELD_NAME ],
51- "High_TH" : data_dict [HIGH_THRESH_FIELD_NAME ],
52- "Low_TH" : data_dict [LOW_THRESH_FIELD_NAME ],
53- "Crit_High_TH" : data_dict [CRIT_HIGH_THRESH_FIELD_NAME ],
54- "Crit_Low_TH" : data_dict [CRIT_LOW_THRESH_FIELD_NAME ],
55- "Warning" : data_dict [WARNING_STATUS_FIELD_NAME ],
56- "Timestamp" : data_dict [TIMESTAMP_FIELD_NAME ]
57- })
116+ data_dict = self .db .get_all (self .db .STATE_DB , key ) or {}
117+ rows .append ({
118+ 'name' : name ,
119+ 'temperature' : data_dict .get (TEMPER_FIELD_NAME , NA ),
120+ 'high_th' : data_dict .get (HIGH_THRESH_FIELD_NAME , NA ),
121+ 'low_th' : data_dict .get (LOW_THRESH_FIELD_NAME , NA ),
122+ 'crit_high_th' : data_dict .get (CRIT_HIGH_THRESH_FIELD_NAME , NA ),
123+ 'crit_low_th' : data_dict .get (CRIT_LOW_THRESH_FIELD_NAME , NA ),
124+ 'warning' : data_dict .get (WARNING_STATUS_FIELD_NAME , NA ),
125+ 'timestamp' : data_dict .get (TIMESTAMP_FIELD_NAME , NA ),
126+ })
127+ return rows
128+
129+ def _collect_sfp_sensors (self ):
130+ """Collect SFP/transceiver temperatures from xcvrd-managed tables.
131+
132+ Map xcvrd threshold fields onto tempershow's columns:
133+ high_th <- temphighwarning
134+ low_th <- templowwarning
135+ crit_high_th <- temphighalarm
136+ crit_low_th <- templowalarm
137+
138+ Warning column is True if any of the temperature high/low
139+ warning/alarm flags in TRANSCEIVER_DOM_FLAG are asserted.
140+ """
141+ rows = []
142+ keys = self .db .keys (self .db .STATE_DB , SFP_TEMPER_TABLE_NAME + '|*' )
143+ if not keys :
144+ return rows
145+ timestamp = datetime .now ().strftime (SFP_DISPLAY_TIME_FORMAT )
146+ for key in natsorted (keys ):
147+ key_list = key .split ('|' )
148+ if len (key_list ) != 2 :
149+ print ('Warn: Invalid key in table {}: {}' .format (SFP_TEMPER_TABLE_NAME , key ))
150+ continue
151+ port = key_list [1 ]
152+ temp_dict = self .db .get_all (self .db .STATE_DB , key ) or {}
153+ thr_dict = self .db .get_all (
154+ self .db .STATE_DB ,
155+ '{}|{}' .format (SFP_THRESHOLD_TABLE_NAME , port )) or {}
156+ flag_dict = self .db .get_all (
157+ self .db .STATE_DB ,
158+ '{}|{}' .format (SFP_FLAG_TABLE_NAME , port )) or {}
159+ rows .append ({
160+ 'name' : self ._sfp_display_name (port ),
161+ 'temperature' : temp_dict .get (SFP_TEMPER_FIELD_NAME , NA ),
162+ 'high_th' : thr_dict .get (SFP_TEMP_HIGH_WARN_FIELD , NA ),
163+ 'low_th' : thr_dict .get (SFP_TEMP_LOW_WARN_FIELD , NA ),
164+ 'crit_high_th' : thr_dict .get (SFP_TEMP_HIGH_ALARM_FIELD , NA ),
165+ 'crit_low_th' : thr_dict .get (SFP_TEMP_LOW_ALARM_FIELD , NA ),
166+ 'warning' : self ._derive_sfp_warning (flag_dict ),
167+ 'timestamp' : timestamp ,
168+ })
169+ return rows
170+
171+ @staticmethod
172+ def _derive_sfp_warning (flag_dict ):
173+ """Return True/False/N/A based on temp flags in TRANSCEIVER_DOM_FLAG.
174+
175+ True - any of temphigh/lowwarning/alarm is asserted
176+ False - all four flags are present and de-asserted
177+ N/A - none of the four flags are present
178+ """
179+ if not flag_dict :
180+ return NA
181+ seen = False
182+ for field in SFP_TEMP_FLAG_FIELDS :
183+ value = flag_dict .get (field )
184+ if value is None :
185+ continue
186+ seen = True
187+ if str (value ).strip ().lower () == 'true' :
188+ return True
189+ return False if seen else NA
190+
191+ def show (self , output_json ):
192+ rows = self ._collect_platform_sensors () + self ._collect_sfp_sensors ()
193+ if not rows :
194+ print ('Thermal Not detected\n ' )
195+ return
58196
59- else :
60- table .append ((name ,
61- data_dict [TEMPER_FIELD_NAME ],
62- data_dict [HIGH_THRESH_FIELD_NAME ],
63- data_dict [LOW_THRESH_FIELD_NAME ],
64- data_dict [CRIT_HIGH_THRESH_FIELD_NAME ],
65- data_dict [CRIT_LOW_THRESH_FIELD_NAME ],
66- data_dict [WARNING_STATUS_FIELD_NAME ],
67- data_dict [TIMESTAMP_FIELD_NAME ]
68- ))
69-
70197 if output_json :
198+ json_output = [{
199+ "Sensor" : r ['name' ],
200+ "Temperature" : r ['temperature' ],
201+ "High_TH" : r ['high_th' ],
202+ "Low_TH" : r ['low_th' ],
203+ "Crit_High_TH" : r ['crit_high_th' ],
204+ "Crit_Low_TH" : r ['crit_low_th' ],
205+ "Warning" : r ['warning' ],
206+ "Timestamp" : r ['timestamp' ],
207+ } for r in rows ]
71208 print (json .dumps (json_output , indent = 2 ))
72- elif table :
73- print (tabulate (table , header , tablefmt = 'simple' , stralign = 'right' ))
74- else :
75- print ('No temperature data available\n ' )
209+ return
210+
211+ table = [(r ['name' ], r ['temperature' ], r ['high_th' ], r ['low_th' ],
212+ r ['crit_high_th' ], r ['crit_low_th' ], r ['warning' ],
213+ r ['timestamp' ]) for r in rows ]
214+ print (tabulate (table , header , tablefmt = 'simple' , stralign = 'right' ))
215+
76216
77-
78217if __name__ == "__main__" :
79218 parser = argparse .ArgumentParser (description = 'Display the temperature Sensor information' ,
80219 formatter_class = argparse .RawTextHelpFormatter ,
0 commit comments