一个保护笔记本电池的小程序
标签:
引发电池早衰的原因无外乎过充和过放。在手机上有一类保护电池的APP。这类APP的逻辑其实很简单,就是当充电到某个阈值的时候提醒用户停止充电,避免充到100%。
这种做法基于某种现象或者是理论。即,锂离子电池不耐受过充、过放的特性。随着充电时间的增加,对电池寿命的损耗是非线性增长的。假设电池充电到100%为一个充电周期,它对电池寿命的损耗为1%,当手机充电到80%,它对电池寿命的损耗不是0.8%,而是只有大约0.2%。利用这个特性,就可以最大化地延长电池的使用寿命。这类电池保护APP别看界面多花哨,它的核心逻辑其实很简单:当使用到20%的时候提醒用户充电,避免过放;当充电到80%的时候提醒用户停止充电,防止过充。
这里有篇文章把这个问题讲得比较清楚:https://baiyunju.cc/9080
手机和笔记本电脑用的都是锂离子电池,原理都是一样的。不同的是,电脑的耗电量远大于手机,续航能力远弱于手机。这意味着,在重度使用的情况下,电脑的电池寿命损耗压力远大于手机。而且电池的更换成本也远大于手机。事实上,保护笔记本电池的需求远强于手机。PC上其实也有保护电池的APP,不过大多是垃圾软件。既然如此,那就自己动手实现吧。
Linux版
Linux版是用Python写的,因为Linux下执行脚本很方便。
#!/usr/bin/env python3
import os
from time import sleep
SYS_PREFIX = '/sys/class/power_supply'
MAX = 80
MIN = 20
WAIT = 3 * 60
# 返回交流电的设备名及状态,True 表示接通,False 表示未接通
def detect_ac_adapter():
for device in os.listdir(SYS_PREFIX):
device_path = os.path.join(SYS_PREFIX, device)
if os.path.isdir(device_path):
status_file = os.path.join(device_path, 'online')
if os.path.isfile(status_file):
with open(status_file, 'r') as f:
status = f.read().strip()
return device, status == '1'
def detect_battery():
for device in os.listdir(SYS_PREFIX):
device_path = os.path.join(SYS_PREFIX, device)
if os.path.isdir(device_path):
status_file = os.path.join(device_path, 'capacity')
if os.path.isfile(status_file):
with open(status_file, 'r') as f:
return device, int(f.read().strip())
return None, None
def send_notification(message):
os.system("notify-send '{}' '{}'".format('充电提醒', message))
def performance_check():
ac_adapter, ac_online = detect_ac_adapter()
battery, capacity = detect_battery()
if battery and capacity <= MIN and (not ac_online):
send_notification(f'电量少于{MIN}%,请充电!')
if battery and capacity >= MAX and ac_online:
send_notification(f'电量大于{MAX}%,请移除外接电源!')
if __name__ == '__main__':
while True:
performance_check()
sleep(WAIT)
这个Python脚本很简单,就是从/sys/class/power_supply
下读取电源信息,当充电小于20%或大于80%发出提醒。只要用某种方式让它定期运行就行了。本来用shell脚本写会更简短,不过我不想写shell脚本。
Windows版
下面是用 racket 重写的 Windows 版。之所以用 racket 重写,是因为用Python生成的exe被Windows反病毒软件误报有病毒。而racket可以方便地生成 exe。
#lang racket/base
(require ffi/unsafe)
(require ffi/unsafe/define)
(require racket/system)
(require racket/port)
;; 定义 POWERSTATUS 结构体
(define-cstruct _POWERSTATUS
([ACLineStatus _ubyte] ; 交流电源状态
[BatteryFlag _ubyte] ; 电池标志
[BatteryLifePercent _ubyte] ; 电池剩余百分比
[BatteryLifeTime _ulong] ; 电池剩余时间(秒)
[BatteryFullLifeTime _ulong])) ; 电池满充时的持续时间(秒)
;; 定义 GetSystemPowerStatus 函数签名
(define-ffi-definer define-kernel32 (ffi-lib "kernel32"))
(define-kernel32 GetSystemPowerStatus (_fun _pointer -> _int))
(define STATUS (make-POWERSTATUS 0 0 0 0 0))
(define MAX 80)
(define MIN 20)
(define WAIT (* 3 60))
(define (send-desktop-notification title message)
;; 构建 PowerShell 脚本字符串
(define powershell-script
(string-append
"powershell.exe -Command "
"[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms');"
"[System.Windows.Forms.NotifyIcon] $notifyIcon = New-Object System.Windows.Forms.NotifyIcon;"
"$notifyIcon.Icon = [System.Drawing.SystemIcons]::Information;"
"$notifyIcon.BalloonTipTitle = '" title "';"
"$notifyIcon.BalloonTipText = '" message "';"
"$notifyIcon.Visible = $true;"
"$notifyIcon.ShowBalloonTip(10000)"))
(unless (system powershell-script)
(eprintf "Failed to send desktop notification.\n")))
(define (check-status)
(let ((result (GetSystemPowerStatus STATUS)))
(if (zero? result)
(printf "Failed to get system power status.\n")
(when (not (= (POWERSTATUS-BatteryLifePercent STATUS) 128))
(when (and (>= (POWERSTATUS-BatteryLifePercent STATUS) MAX)
(= (POWERSTATUS-ACLineStatus STATUS) 1))
(let ((msg (format "电量高于~a%, 为保护电池,请及时移除充电器。" MAX)))
(send-desktop-notification "充电提醒" msg)))
(when (and (<= (POWERSTATUS-BatteryLifePercent STATUS) MIN)
(= (POWERSTATUS-ACLineStatus STATUS) 0))
(let ((msg (format "电量不足~a%, 为避免过度放电,请及时充电。" MIN)))
(send-desktop-notification "充电提醒" msg)))))))
(do () (#f)
(check-status)
(sleep WAIT))
Windows麻烦一点,电池信息是通过Windows API GetSystemPowerStatus 获得的,通知是通过 powershell 脚本发送的。
编译成独立exe后,搭配下面的 vbs 脚本,可以实现无界面在后台运行。
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "batsafe.exe", 0, False
Racket的运行时优化是比较扯蛋的,一个小脚本要占用100多M的内存。刚刚发现一个语言:Crystal,语法和Ruby差不多,但是可以静态编译。用Crystal重写后,同样的程序,占用内存空间不到1M,800K的样子。立刻就喜欢上这个语言了。下面是编译后的程序:
下载链接: battery_safe.zip
下载解压到某个目录,然后:
右键点击“battery_safe.vbs”->发送到->桌面快捷方式
Win + R,打开“运行”对话框,输入:shell:startup,点击“确定”,打开浏览器窗口,路径已经定位到"...\Windows\开始菜单\程序\启动".
把刚刚建立的快捷方式拖入“启动”文件夹中,实现开机启动。
Crystal 版源码
MAX = 80
MIN = 20
WAIT = 3 * 60
def send_notification(title, message)
ps = "powershell.exe -Command
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
[System.Windows.Forms.NotifyIcon] $notifyIcon = New-Object System.Windows.Forms.NotifyIcon;
$notifyIcon.Icon = [System.Drawing.SystemIcons]::Information;
$notifyIcon.BalloonTipTitle = '#{title}';
$notifyIcon.BalloonTipText = '#{message}';
$notifyIcon.Visible = $true;
$notifyIcon.ShowBalloonTip(10000)"
system(ps)
end
@[Link("kernel32")]
lib Kernel32
# 定义 SYSTEM_POWER_STATUS 结构体
struct SystemPowerStatus
ac_line_status : UInt8 # AC电源状态
battery_flag : UInt8 # 电池状态标志
battery_life_percent : UInt8 # 电池剩余电量百分比
reserved1 : UInt8 # 保留字段
battery_life_time : UInt32 # 电池剩余时间(秒)
battery_full_life_time : UInt32 # 电池充满电的时间(秒)
end
# 定义 GetSystemPowerStatus 函数
fun GetSystemPowerStatus(lpSystemPowerStatus : SystemPowerStatus*) : Int32
end
def run_check
status = Kernel32::SystemPowerStatus.new
if Kernel32.GetSystemPowerStatus(pointerof(status)) != 0
if status.battery_flag != 128
if status.battery_life_percent >= ::MAX && status.ac_line_status == 1
send_notification("充电提醒", "电量大于#{::MAX}, 请尽快移除充电器!")
end
if status.battery_life_percent <= ::MIN && status.ac_line_status != 1
send_notification("充电提醒", "电量不足#{::MIN}, 请尽快充电!")
end
end
end
end
while true
run_check
sleep ::WAIT.seconds
end
基本原理是通过Win32 API查询电池信息,再用 PowerShell 脚本发送桌面通知。绕路绕得有点远。。。