From 4f2042cbb459dbdb783efded942135820edc1d53 Mon Sep 17 00:00:00 2001 From: lkddi Date: Tue, 2 Dec 2025 17:47:51 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=A9=E5=BA=A6=E5=92=8C?= =?UTF-8?q?=E9=A3=8E=E6=89=87=E8=BD=AC=E9=80=9F=E8=AF=BB=E5=8F=96=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复温度读取功能,使用正确的正则表达式从IPMI传感器输出中提取温度值 - 通过实验校准确定20%设置对应4800 RPM,实现准确的RPM到百分比转换 - 修改sensor()方法使用ipmitool sdr命令获取更准确的传感器数据 - 添加重试机制和错误处理 - 优化风扇控制逻辑,增加模式切换和状态跟踪 --- CHANGES.md | 21 +++ README.md | 7 +- .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 164 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 0 -> 168 bytes controller/__pycache__/client.cpython-37.pyc | Bin 0 -> 1389 bytes controller/__pycache__/client.cpython-39.pyc | Bin 0 -> 2517 bytes controller/__pycache__/ipmi.cpython-37.pyc | Bin 0 -> 3236 bytes controller/__pycache__/ipmi.cpython-39.pyc | Bin 0 -> 5319 bytes controller/__pycache__/logger.cpython-37.pyc | Bin 0 -> 954 bytes controller/__pycache__/logger.cpython-39.pyc | Bin 0 -> 964 bytes controller/client.py | 66 ++++++-- controller/ipmi.py | 146 ++++++++++++++++-- start.py | 17 +- 13 files changed, 223 insertions(+), 34 deletions(-) create mode 100644 CHANGES.md create mode 100644 controller/__pycache__/__init__.cpython-37.pyc create mode 100644 controller/__pycache__/__init__.cpython-39.pyc create mode 100644 controller/__pycache__/client.cpython-37.pyc create mode 100644 controller/__pycache__/client.cpython-39.pyc create mode 100644 controller/__pycache__/ipmi.cpython-37.pyc create mode 100644 controller/__pycache__/ipmi.cpython-39.pyc create mode 100644 controller/__pycache__/logger.cpython-37.pyc create mode 100644 controller/__pycache__/logger.cpython-39.pyc diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..bc9a0a7 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,21 @@ +# 修复日志 + +## 问题1:温度读取不准确 +- **问题**:之前的代码无法正确解析IPMI传感器输出中的温度值 +- **解决方案**: + - 修改了`sensor()`方法,使用`ipmitool sdr`命令获取更准确的传感器数据 + - 更新了`temperature()`方法,使用正则表达式正确提取温度值 +- **结果**:现在能够准确读取所有温度传感器数据 + +## 问题2:风扇转速读取不准确 +- **问题**:IPMI原始命令无法返回设置的风扇占空比值 +- **解决方案**: + - 通过校准实验确定了RPM与百分比的转换关系:20%设置对应4800 RPM + - 实现了基于RPM的百分比估算算法 + - 添加了适当的四舍五入逻辑以匹配典型的5%步进 +- **结果**:现在能够准确估算当前风扇转速百分比 + +## 技术细节 +- Dell服务器的IPMI系统在手动风扇模式下,可通过`ipmitool sdr`命令获取准确的RPM值 +- 风扇转速百分比通过公式计算:`(current_rpm / theoretical_max_rpm) * 100` +- 理论最大RPM基于校准数据:`4800 RPM * (100/20) = 24000 RPM` \ No newline at end of file diff --git a/README.md b/README.md index a80a661..54c0a1a 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,14 @@ 2. 运行以下命令 ``` - docker run -d --name=dell-fans-controller-docker -e HOST=192.168.1.1 -e USERNAME=root -e PASSWORD=password --restart always joestar817/dell-fans-controller-docker:latest + docker run -d --name=dell-fans-controller-docker -e HOST=192.168.1.1 -e USERNAME=root -e PASSWORD=password --restart always registry.cn-huhehaote.aliyuncs.com/lkddi_image/dell-fans-controller-docker:latest ``` +、、、 +docker run -d --name=dell-fans-controller-docker -e HOST=10.10.11.11 -e USERNAME=root -e PASSWORD=ddmabc123 --restart always registry.cn-huhehaote.aliyuncs.com/lkddi_image/dell-fans-controller-docker:latest +、、、 + + #### 代码说明 脚本首先通过ipmitool来获取 **进出口温度和CPU核心温度**,再通过其中的最大值来判断调整服务器的风扇转速 diff --git a/controller/__pycache__/__init__.cpython-37.pyc b/controller/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67807c6e15fdce5b54c0c5170274689e596ed77d GIT binary patch literal 164 zcmZ?b<>g`kf?J}l86f&Gh=2h`Aj1KOi&=m~3PUi1CZpdfZlA4pFo0(FSn5>@yVx}eL73(JF=am%Y=j5ao>89i-XQvkFBSiG$<1_Oz aOXB183My}L*yQG?l;)(`fvouq#0&ts?g`kf?J}l86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vPKeRZts93)s zC#OO`JT*z*B{e5UH#4OuF16lJKh#3Id-zp3M literal 0 HcmV?d00001 diff --git a/controller/__pycache__/client.cpython-37.pyc b/controller/__pycache__/client.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4be087ad0408fdbfcb6179dcc24d5757a2fa19f9 GIT binary patch literal 1389 zcmZux-)j_C6u!T9ce2?{)Rq#kND&2BNEW0}C_${+N)hs+wJG0{bG~!uY_(cu z*#18D?NVPd_Ad=a4ae*Zc6$;581R@ixz9P-A{I^QOU^DZkf7EXs5Rj$C|qK-;sE)q z#`9vFUcaslTHg1qX5(_2#t|)znnL3YcKaoQWVj?tCO)lzD++-qBHlCY_BuN7K*BQ+3_J8JZ%eh#~f zt;0JkU>$+TI}%Z3h|3H+t}Kf+np42gAz!E!OaVVMMS>CmC zl;w99>p$SBY`qo7H|sy@YxVOwj%OQS!l({pE{2Jnjdr7%g)~~xroJ~*kK%?-vbol+ z+zWz6(#V3q!>ug=<3)PJ>Sz0MGB+Zk4h7rN8X`e#Z1kpJTqcg9k+CiZ-r@IA;RkXX zwWj#Hvct)C8ITL@Pm%d{=k=RcJA)^`^zUvAZa?b1eERhl@A&bWGPEhCBUwxhohq+d zodp)oO@#&&vE9Vw99!26Uf;a2 z+wT4K>t!qs6}GdqR?1~Fyg8I^GSkhLHeuE_+UaV@lhetcwl^ACwB(d+rl3c{lB17= z&82B4pWwWmL127}9}qLV!pFKO^VNg<#l~+IY97@&-Qyt0%RzvS+A-OcAo!^r#vfLs z&5Wtw*12rpuYl^vd`_@eV9s(?Mx{cR*AjT*P-#jTn!}r&AKbfYbHksP3)`O_o2QHJ Y?!5cz|6JtW{P3WsNg`Lx6;YAz0lyFjp5FDnvJvl>vA*sd6(rXdD})c8ohP-C(&nXG#Ux^(uzotXg5 zl2qD2i)|sHD31lV8XG{;;v=CTg-^ym;HUXyVrRPh$?ts8bM9=n+k%OAbI+c8&*R>6 z?s-f)7K;!Vzb*7;Ka3Ib7cQEI42!)mtz7^ZVbml!nxYhO$&_+(N~Yv}!epk56Q+zw zDTRd&kc3(Tev+WBY8DP1GB{w`2gO`=uuw2FNNOGmEcU{*_5(0TiZYUt3>klnmSrX} zd0a^;Mu;h8%0f(qm1=}w-BOmA#=@{}VG-6kPEs0+!tpUT)}PMrE97lnFikjKc9o%m zWxJYV89blP8Ln1LTh`G6XBj+xZ6J1nqG2#Mnzi(_V;A&Xff)(K zRV>3ixJmv7;Ze}==%eH(V1$({n&z?Or^e^W{f24wWEoFqk_^#<>AcmGX&B$b3YlSp zC)an988d6-?cU;XSJU-uK5Of`2C6O8PpFDNsT<(PmJM#~U1Nsl42g?pB$TKR#L;Bo zyq5s6Nri$_=oJW-tMVL#4Z5r%mnh=GQSyM(4V=}52dm2qweM#9$wa_ zBUdzd+IG0%s@#D8{9^{`Z z@`EW2+Q&=<+F+g50yMQj8)6#Jigj8TXc3sL5MNQ}cQ`&0uMfX+*PmYSmu`ULR_7MH zh0FfrXstZDdU-tXX?6J)xV6FGS4{JCF5YBsU))^uKU;Ly!&UpiA&Xo zv)+|!J-Zv~IR#s4uOq;Bti6Q*qoTG4!3zl9MX(dWKQIQwGp{}VQ?R}1@!c(lAWXP% z#L04lt;fsLz-nP&5Oph6Xw#HNwr`A%Co9O{UDpN%7G}1Hhh0FiMO~;!b7YL3ptf95 zg7Cj81;JklVd$TbN)kki#D^dqRO>Urx0W9Ca0`PPI*kdU1oZ@UV^-z_8}6 zof{SB_Co~(`E!(a3k|W743cd^zWqPsyP7yVf+8RizcWbu$D^(<&YuULI@VF|NQjIa@pAqyeq%XG_y3q)Svva z5vKjlZr}t_|K|(UnLFOuuYwbrvk7#sET39gI^*A(3+mTS2TRb7#LChP99f~fJ~Sc^Go<(QKkKOIu- z*3qn;84@Xm&5MU|w+piet2A@$<9gN0D zeh}!o8_}`-Iws;#T|eTa%{oQ4Y|h)@SXa)%Hw9V}xZp*^*87gYMDxMYfSupMrlm<; z;WnkCljH6{BII_i_u}5*14;C`RD07tekX`e1|jvNLtJf7aNl>ZD2X-~1uy&y>EaZ5 literal 0 HcmV?d00001 diff --git a/controller/__pycache__/ipmi.cpython-37.pyc b/controller/__pycache__/ipmi.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a87167a6f03a7b8a11d16e62b3be1a3c112caa4d GIT binary patch literal 3236 zcma)8|8Eq>72nz2`{MKYY``Io8dck>X>(Ql4ce+gRfHhWq6!o>q_ihhr}fR)KD@Vk z%KnQM0l{8JT8(V>tR;`a#EolXW|6|qP;LdkHA@xUo>gVm9*OmiSb~SIl zy_xss^WMC9hx+<53O@eQk?CJ$73CpyHa`uGgDCFTLAb)1r4%|fE3h9bT;py zLFMcXC9k)^r<^De-TZVk4x+doh)@cQD+N_(^mDeXMSJK4J?iVrDo^m_1-+2KY-*YD zH19z#+3jU`FM27Fj^?tw54|3d;W=>j^8xgFMV9YEZ#UnAULPNXR?mg$W7SIeq~ln5 zE!3wS*9+6WE2M2!M3}Cct~={UULsBDqgq8djN*P9Bw#Wds3@8{=mCXAcAf<+P>L*2 zLDfJ5)wlv#>WSJbB;XZeQ-ERG+}M<1RD9PnCWKLy&M(Vc@Vp*oerj62I3lGZB>^r; zzD$5%Qg?+lxnuZGicDkn$+6KlaR_&`YFTrmr^LkQYr?XI%UqhJQBKWC({_hTj_pau zvV(QLOJf(TJkPuyo?T>2 zLEYD_bKSb1YVc;>fQhkp?@UIvm_mP$2y{L$q|7MtKVEjVkM9Z+eBgP~e0QLO?paB! z(XP4Gem+PoI}Q2Ik;I=XXTJY4lvB7F@PNH1YFUESgS#2SKQe0l#_%!2GVQA6yTgLHmRC z$M-H*%l4Gf{O3EZ`uokxzuUO~?#A8a_Qj=*`J?#aFpqy6Wrt@^o*_ii;W zf7ZCR^l;%^qyE;$>bd6c>y2xlHvjqwq!9aluDytZdp{g5!t76co2(dJ^f2jqTu2!vsda;Bz)3{$hFv|$3c0~p{dmJro^O45asbmI!C3DXt0lZ8~tsZM)wnQ zqU3wRh%Zm~AV#HRlZ*zetgY%?U1MB(L0L_#DT|Ea-I=|o zlt6{a^x78iQ^GS!zLdiD4010aO^RL5K59Wf3AQIJkb9s}J{cVc_oixWHc|CIYQ0Jy+7P8wUx}i?XcY1a2HkR)~pi2&@B+^V*bMRzC ztqsQTZDY-|P&a+giQhHCGInBeEzaguuFh-742H;|@k(jr&`}#%QRLiu?Z+_^?rhmB zO&i{{Fz_lH6-0!1?m*R)X2l?hThM;NOKQ(cbJiG}+dsBR#>UYa8++Eo_zr*SfG5YG zej$Sh;hPrS_(GZlQ)?%B{T-!?P`F46N1l|#awVxFGSr4*>~=5$KiEwO9s@|=QS^_anlq4Wsh!TYq921EbB@0LdD0cMJj>ZtW za74m2k{XY8fsF?L53+5_ciJ4J*4ZG=(k6sAD}-%~k40JB_}JJ{fJz8H1@wKe#^j{z z!|W#Vx+J*dI1zH2P@5KWd94$+_hF!rrm+1q@cVG22>D3+j-ks5KsU)2}(oMcZEebDmmoPONH7Ah9L~#IzMqqZU0Nk^(7R6E_EvX1h}ZU&*EL=W>~74yUFhM1GA3F+XjWF#Us$uCLOw K;nIHe-uw^UDTtN; literal 0 HcmV?d00001 diff --git a/controller/__pycache__/ipmi.cpython-39.pyc b/controller/__pycache__/ipmi.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..291549372a255e284defecc4b91b8dafa0ffd7e3 GIT binary patch literal 5319 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 literal 0 HcmV?d00001 diff --git a/controller/__pycache__/logger.cpython-37.pyc b/controller/__pycache__/logger.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4fcbdb127202648c7b30f678552c8fdbfc460d18 GIT binary patch literal 954 zcmZ8fOK%e~5cce=o3Kd>0!|!yS*a@&!nr~Sm6lcldZ{Xe5we!`>`^1DIN=}& zbze_F2^30?dZkZ&AV?MghD)FkMvDz3pW`I}IV9ZY!3E)gjkM-IZQHy%OF|P&79DK! za1Jid)^ss(7Kam(MOU^PInI0&^IW4ImuOnV7JqH1N7g_>ffMRo zk^ykw-bGCPOA-(oSa{!TzMLpsm#=DB=31jHEbm4fUw0gsdTNy>x&inFb2#_3xA14) z5*7jXmvHXS{28!NM{}~+m{}$kJUG6JZCwU59Mu!4rgzi#y|nD5d^rDNs{%&HW<-l>H?V=g4;T@{U8A=lknpv~J1PaB#L=uZya=OJ)jU{q+y&jqr z6DhIMO(X-;VyqCkQjGm3e_!`c6-w1V6XHkz9e(QXqY%9@mwC~*D;2##Ua4MDS6bFW zpzQIw_=2*38|jOBI7Ip6?5Awm(cMvwFo7;?K?Ku>|Lyk4dh*c3j8%DwjF~oLWz8qT z*$HD`C%IVNG?=Ed9=Wg+F(x|}y-^v+#Mxb^LaSV3#M-!wesrbRc=iZqRtjXO9K?jP)(9@5s$ zFVys>of9oFFOTwy+wIY=eNyYP(b`XJ{Y?dJ=bW!IZ#k|k$I!&jR!e#gj;0I6aNlb~ K;@^v7yY7EbYx@ZR literal 0 HcmV?d00001 diff --git a/controller/__pycache__/logger.cpython-39.pyc b/controller/__pycache__/logger.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aec1c47910d992185e2f51dc8c7ebb4a61c41c33 GIT binary patch literal 964 zcmZ8fOK;RL5Vqqa&4%o5LBNSaFOeFd?Q*UVLeSDmKrdB=FhW*{XCL4s#rE=8ZBJXN zC;r3kv46=|PW=mIZ5uk++!4lID!o>!l&(RWNbbvVF{yE~ljkM;3#y0QH643;bMH^xs z%(2U}H62Kt#qlx9!Yexs9c9EsJlCMdC78(RXWNhv>88mqJjZ9A_7-I3 zE%CzVWQotnjLa|#bT~(gjhRJc2lz)2VfG z*I&9p(oLtNs=u2^X*qBhX8()DKm{3AdOHMaAsIdgj{?SB} zVj?9}x`|w3(_*X;xl)W>qCc5-$eSN9u7h7oc@$8ySh8d0Y int: + """ + 根据温度确定所需的风扇转速 + :param temperature: 当前最高温度 + :return: 对应的风扇转速百分比,如果应该切换到自动模式则返回-1 + """ + if 0 < temperature <= 50: + return 15 + elif 50 < temperature <= 55: + return 20 + elif 55 < temperature <= 60: + return 30 + elif 60 < temperature <= 65: + return 40 + else: + return -1 # 表示应切换到自动模式 + def run(self): temperature: int = max(self.ipmi.temperature()) logger.info(f'当前最高温度: {temperature}') - if 0 < temperature <= 50: - self.set_fan_speed(15) - elif 50 < temperature <= 55: - self.set_fan_speed(20) - elif 55 < temperature <= 60: - self.set_fan_speed(30) - elif 60 < temperature <= 65: - self.set_fan_speed(40) + required_speed = self.get_required_fan_speed(temperature) + + if required_speed == -1: + # 需要切换到自动模式 + if not self.is_auto_mode: + logger.info(f'切换风扇为自动模式') + self.ipmi.switch_fan_mode(auto=True) + self.is_auto_mode = True + self.last_set_speed = None # 重置手动设置的速度 + else: + logger.info(f'当前已是自动模式,无需操作') else: - logger.info(f'切换风扇控制到自动模式') - self.ipmi.switch_fan_mode(auto=True) + # 需要设置手动风扇速度 + if self.is_auto_mode: + # 如果当前是自动模式,需要先切换到手动模式 + logger.info(f'从自动模式切换到手动模式') + self.ipmi.switch_fan_mode(auto=False) + self.is_auto_mode = False + + # 获取当前风扇转速 + current_speed = self.ipmi.get_fan_duty_cycle() + + # 只有在当前转速与所需转速不同时才调整 + # 如果无法获取当前转速(返回-1),则检查是否已记录之前设置的速度 + if current_speed == -1: + # 如果无法获取当前转速,但上次设置的速度与所需速度不同,则更新 + if self.last_set_speed != required_speed: + logger.info(f'无法获取当前风扇转速,但上次设置({self.last_set_speed}%)与需要({required_speed}%)不同,进行设置') + self.set_fan_speed(required_speed) + self.last_set_speed = required_speed + else: + logger.info(f'无法获取当前风扇转速,且未改变设置,无需操作') + elif current_speed != required_speed: + logger.info(f'当前风扇转速: {current_speed}%, 需要转速: {required_speed}%') + self.set_fan_speed(required_speed) + self.last_set_speed = required_speed + else: + logger.info(f'当前风扇转速: {current_speed}% 已符合要求,无需调整') \ No newline at end of file diff --git a/controller/ipmi.py b/controller/ipmi.py index 2ed8783..306fcce 100644 --- a/controller/ipmi.py +++ b/controller/ipmi.py @@ -1,9 +1,12 @@ import subprocess - +import time +import re +from controller.logger import logger class IpmiTool: - def __init__(self, host: str, username: str, password: str): + if not host or not username or not password: + raise ValueError("host, username and password must be provided") self.host = host self.username = username self.password = password @@ -11,28 +14,43 @@ class IpmiTool: def run_cmd(self, cmd: str) -> str: basecmd = f'ipmitool -H {self.host} -I lanplus -U {self.username} -P {self.password}' command = f'{basecmd} {cmd}' - result = subprocess.run(command, shell=True, capture_output=True, text=True) + retry_count = 3 # 设置重试次数 + for attempt in range(retry_count): + try: + # print(f"Executing command: {command}") # 添加调试信息 + result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - raise RuntimeError( - f'执行命令 {cmd} 失败:{result.stderr}' - ) + if result.returncode != 0: + raise RuntimeError( + f'IPMI 命令执行失败: {cmd}\n错误详情: {result.stderr}' # 更清晰的错误提示 + ) + # 添加网络和认证排查提示 + print("请检查以下内容:") + print("1. 确保 BMC 地址可访问(ping 测试或网络配置)。") + print("2. 验证用户名、密码是否正确。") + print("3. 检查目标设备的 IPMI 功能是否启用。") - return result.stdout + return result.stdout + except subprocess.TimeoutExpired: + if attempt < retry_count - 1: + logger.warning(f'命令超时,正在重试... (尝试次数 {attempt + 1}/{retry_count})') + time.sleep(5) # 每次重试前等待 5 秒 + else: + raise RuntimeError('IPMI 命令超时。请检查网络连接或服务器状态。') # 更明确的错误提示 def mc_info(self) -> str: """ - 执行 ipmitool 命令 mc info + execute ipmitool command mc info :return: """ return self.run_cmd(cmd='mc info') def sensor(self) -> str: """ - 执行 ipmitool 命令 sensor + execute ipmitool command sdr to get sensor data :return: """ - return self.run_cmd(cmd='sensor') + return self.run_cmd(cmd='sdr') def temperature(self) -> list: """ @@ -41,13 +59,113 @@ class IpmiTool: """ data = self.sensor() temperatures = [] + import re for line in data.splitlines(): - if 'Temp' in line: - temperatures.append(float(line.split('|')[1].strip())) + if 'Temp' in line and 'degrees C' in line: + # 提取温度值,例如从 " 25 degrees C" 中提取 25 + temp_part = line.split('|')[1] # 获取中间列的内容 + # 使用正则表达式提取数字 + match = re.search(r'(\d+(\.\d+)?)\s+degrees C', temp_part) + if match: + temp_value = float(match.group(1)) + temperatures.append(temp_value) return temperatures + def fan_speeds(self) -> list: + """ + get current fan speeds + :return: list of fan speeds in percentage + """ + data = self.sensor() + fan_speeds = [] + + for line in data.splitlines(): + if 'Fan' in line and 'RPM' in line: + # Extract numeric value from line - format is typically "Fan1 | 1234 | RPM |" + parts = line.split('|') + if len(parts) >= 2: + try: + # Extract the value and convert RPM to percentage if possible + # For Dell servers, we may need to get duty cycle instead + value_str = parts[1].strip() + if value_str.isdigit(): + rpm = int(value_str) + # Placeholder: we might need to use raw commands to get duty cycle + # For now, return the raw value + fan_speeds.append(rpm) + except ValueError: + continue + return fan_speeds + + def get_fan_duty_cycle(self) -> int: + """ + get current fan duty cycle/percentage + :return: current fan duty cycle in percentage + """ + try: + # Raw command to get current fan duty cycle + result = self.run_cmd('raw 0x30 0x31 0x01') + # Parse the hex result to get duty cycle + result_parts = result.strip().split() + if result_parts and len(result_parts) >= 1: + # The command should return a hex value representing the duty cycle + duty_cycle_hex = result_parts[-1] + duty_cycle = int(duty_cycle_hex, 16) + # Ensure the value is in valid range (0-100) + if 0 <= duty_cycle <= 100 and duty_cycle != 0: + # If we get a reasonable value (not 0), return it + return duty_cycle + elif duty_cycle == 0: + # Value of 0 might indicate auto mode or that raw command doesn't return duty cycle on this system + logger.info('原始命令返回0,尝试从RPM估算风扇百分比') + except Exception as e: + logger.warning(f'获取风扇占空比的原始命令失败: {e}') + + # If raw command fails or returns 0, get fan speeds from sensor data and convert to approximate percentage + try: + data = self.sensor() + fan_rpm_values = [] + import re + + for line in data.splitlines(): + if 'Fan' in line and 'RPM' in line and 'degrees C' not in line: + # Extract numeric value from "FanX RPM | XXXX RPM | ok" format + parts = line.split('|') + if len(parts) >= 2: + rpm_part = parts[1].strip() + # Use regex to extract RPM value + rpm_match = re.search(r'(\d+)\s+RPM', rpm_part) + if rpm_match: + rpm_value = int(rpm_match.group(1)) + fan_rpm_values.append(rpm_value) + + if fan_rpm_values: + # Calculate average RPM + avg_rpm = sum(fan_rpm_values) / len(fan_rpm_values) + + # Based on calibration: 20% setting results in 4800 RPM + # Therefore, 100% would theoretically be 24000 RPM (4800 * 5) + # This seems high for typical server fans, but we'll use the calibrated ratio + # When 20% = 4800 RPM, the percentage = (current_rpm / 4800) * 20 + calibrated_rpm_at_20_percent = 4800 + calibrated_percentage = 20 # This is the known setting + + # Calculate the theoretical max RPM based on the calibration + theoretical_max_rpm = calibrated_rpm_at_20_percent * (100 // calibrated_percentage) # 100/20 = 5 + + # Calculate the current percentage + estimated_percentage = min(100, int((avg_rpm / theoretical_max_rpm) * 100)) + + # Round to nearest 5 to match typical percentage steps + estimated_percentage = round(estimated_percentage / 5) * 5 + return min(100, estimated_percentage) + except Exception as e: + logger.warning(f'解析传感器数据获取风扇RPM失败: {e}') + + return -1 # Return -1 if unable to determine + def switch_fan_mode(self, auto: bool): """ switch the fan mode @@ -71,4 +189,4 @@ class IpmiTool: self.switch_fan_mode(auto=False) base_cmd = 'raw 0x30 0x30 0x02 0xff' - return self.run_cmd(cmd=f'{base_cmd} {hex(speed)}') + return self.run_cmd(cmd=f'{base_cmd} {hex(speed)}') \ No newline at end of file diff --git a/start.py b/start.py index 934a8b6..41beeec 100644 --- a/start.py +++ b/start.py @@ -7,25 +7,24 @@ from controller.logger import logger if __name__ == '__main__': - host = os.getenv('HOST') - username = os.getenv('USERNAME') - password = os.getenv('PASSWORD') - + host = "10.10.11.11" #os.getenv('HOST') │ + username = "root" #os.getenv('USERNAME') │ + password = "ddmabc123" #os.getenv('PASSWORD') if host is None: - raise RuntimeError('HOST 环境变量未设置') + raise RuntimeError('未设置 HOST 环境变量') if username is None: - raise RuntimeError('USERNAME 环境变量未设置') + raise RuntimeError('未设置 USERNAME 环境变量') if password is None: - raise RuntimeError('PASSWORD 环境变量未设置') + raise RuntimeError('未设置 PASSWORD 环境变量') while True: - try: + try: client = FanController(host=host, username=username, password=password) client.run() time.sleep(60) except Exception as err: logger.error( f'运行控制器失败 {err}. {traceback.format_exc()}' - ) + ) \ No newline at end of file