一个保护笔记本电池的小程序

标签:


引发电池早衰的原因无外乎过充和过放。在手机上有一类保护电池的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

下载解压到某个目录,然后:

  1. 右键点击“battery_safe.vbs”->发送到->桌面快捷方式

  2. Win + R,打开“运行”对话框,输入:shell:startup,点击“确定”,打开浏览器窗口,路径已经定位到"...\Windows\开始菜单\程序\启动".

  3. 把刚刚建立的快捷方式拖入“启动”文件夹中,实现开机启动。

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 脚本发送桌面通知。绕路绕得有点远。。。