Perusing the Linux Kernel source code: Setting battery charge threshold on ASUS laptops

Using systemd to set battery charge threshold automatically during booting on compatible ASUS laptops running Linux Kernel version 5.4 and above. Also, perusing the Linux Kernel source code to understand the history of "battery charge threshold" feature on ASUS laptops and how it works.

My ASUS VivoBook X407UAR laptop supports ASUS Battery Health Charging available in the MyASUS software. This software helps users extract maximum life out of a battery. However, MyASUS software works only on Windows. This article explains how to set a charge threshold on compatible ASUS laptops running openSUSE Tumbleweed. Any Linux distribution running Linux Kernel 5.4 and above should support this functionality.

For all this to work on Windows, ASUS supplies an ACPI BIOS with WMI objects embedded in it. ACPI is an open standard while WMI is a proprietary implementation of another open standard called CIM. So, on Windows, the ACPI BIOS with embedded WMI objects interfaces seamlessly. However, all of this makes it difficult for the Linux kernel to interface with platform devices. You know where I am going with this ;)

Setting a charge threshold using a systemd service

Check the battery name

Confirm if laptop supports ASUS Battery Health Charging. If it does, find the battery name:

ls /sys/class/power_supply/

In my case it returns:

AC0  BAT0
🚧
If the battery name returned is something other than BAT0, read the "Will my Linux Kernel version support setting battery charge threshold?" section for more details.

To be double sure, list what is available in BAT0:

ls /sys/class/power_supply/BAT0

If the output contains charge_control_end_threshold, we are good to go. If it does not, then one of the following statements must be true:

  • Linux Kernel version running on the laptop is older than 5.4;
  • The laptop does not support ASUS Battery Health Charging.

Deploy a systemd unit file

Create a systemd unit file with the following content:

[Unit]
Description=Set the battery charge threshold
After=multi-user.target
StartLimitBurst=0

[Service]
Type=oneshot
Restart=on-failure
ExecStart=/bin/bash -c 'echo 60 > /sys/class/power_supply/BAT0/charge_control_end_threshold'

[Install]
WantedBy=multi-user.target

I have set the threshold to 60 percent because that is exactly what the MyASUS software would do in Maximum Lifespan Mode on Windows.

Save it as /etc/systemd/system/battery-charge-threshold.service. The unit file can be named anything.

Enable and start the service:

systemctl enable battery-charge-threshold.service
systemctl start battery-charge-threshold.service

If no mistakes were made in following the above steps, the following should be the result:

cat /sys/class/power_supply/BAT0/status
Not charging

My GNOME 41 DE also prominently displays the status of the battery as Not charging in the UI.

Will my Linux Kernel version support setting battery charge threshold?

Examine the history of charge threshold in Linux source

I had tried setting a charge threshold on the same laptop when it was running openSUSE Leap 15.3; openSUSE Leap uses 5.3.18 Linux Kernel which obviously does not support the charge threshold. It worked fine with the MX Linux Advanced Hardware Support edition as long as I used it. I then moved to openSUSE Tumbleweed.

For those who like to dig deeper, git clone the Linux source code. git commands will help in obtaining various details about the history of linux/drivers/platform/x86/asus-wmi.c. An example is given below.

git log --all drivers/platform/x86/asus-wmi.c | grep -i "charge threshold"

The output is:

The WMI method to set the charge threshold does not provide a
Fixes: 7973353e92ee ("platform/x86: asus-wmi: Refactor charge threshold to use the battery hooking API")
platform/x86: asus-wmi: Refactor charge threshold to use the battery hooking API
platform/x86: asus-wmi: Add support for charge threshold

Further, pick the oldest commit message and search:

git log --grep="asus-wmi: Add support for charge threshold"

This command gives the following output:

commit d507a54f5865d8dcbdd16c66a1a2da15640878ca
Author: Kristian Klausen kristian@klausen.dk
Date:   Mon Aug 5 21:23:05 2019 +0200

platform/x86: asus-wmi: Add support for charge threshold

Most newer ASUS laptops supports limiting the battery charge level, which
help prolonging the battery life.

Tested on a Zenbook UX430UNR.

Signed-off-by: Kristian Klausen <kristian@klausen.dk>
Signed-off-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com>

Pick the commit hash (d507a54f5865d8dcbdd16c66a1a2da15640878ca, in this case) and do the following to see the changes compared to previous version:

git show d507a54f5865d8dcbdd16c66a1a2da15640878ca 

Now, it is time to check what Linux Kernel versions this commit found its way into. The oldest is the most relevant in our investigation:

git tag --contains d507a54f5865d8dcbdd16c66a1a2da15640878ca

Use GitHub for better usability; terminal for speed

Once the commit hash is determined using the terminal, it can be used in GitHub to analyze the history of ASUS ACPI WMI kernel drive/module. GitHub hosts a mirror of Linux Kernel and has a much better user experience than the terminal.

Latest Linux Kernel 5.9 and above

The latest Linux Kernel supports all available battery names for ASUS laptops that support ASUS Battery Health Charging (BAT0, BAT1, BATC and BATT). BATC support added via commit 1d2dd379bd99ee4356ae4552fd1b8e43c7ca02cd. It is quite evident if we analyze linux/drivers/platform/x86/asus-wmi.c:

static int asus_wmi_battery_add(struct power_supply *battery)
{
	/* The WMI method does not provide a way to specific a battery, so we
	 * just assume it is the first battery.
	 * Note: On some newer ASUS laptops (Zenbook UM431DA), the primary/first
	 * battery is named BATT.
	 */
	if (strcmp(battery->desc->name, "BAT0") != 0 &&
	    strcmp(battery->desc->name, "BAT1") != 0 &&
	    strcmp(battery->desc->name, "BATC") != 0 &&
	    strcmp(battery->desc->name, "BATT") != 0)
		return -ENODEV;

	if (device_create_file(&battery->dev,
	    &dev_attr_charge_control_end_threshold))
		return -ENODEV;

	/* The charge threshold is only reset when the system is power cycled,
	 * and we can't get the current threshold so let set it to 100% when
	 * a battery is added.
	 */
	asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, 100, NULL);
	charge_end_threshold = 100;

	return 0;
}

Arch Linux and its derivatives, or openSUSE Tumbleweed or Debian Sid or Fedora have full support.

Linux Kernel 5.8 and above

BAT0, BATT and BAT1 battery names supported. BAT1 support added via commit 9a33e375d98ece5ea40c576eabd3257acb90c509:

static int asus_wmi_battery_add(struct power_supply *battery)
{
	/* The WMI method does not provide a way to specific a battery, so we
	 * just assume it is the first battery.
	 * Note: On some newer ASUS laptops (Zenbook UM431DA), the primary/first
	 * battery is named BATT.
	 */
	if (strcmp(battery->desc->name, "BAT0") != 0 &&
	    strcmp(battery->desc->name, "BAT1") != 0 &&
	    strcmp(battery->desc->name, "BATT") != 0)
		return -ENODEV;

	if (device_create_file(&battery->dev,
	    &dev_attr_charge_control_end_threshold))
		return -ENODEV;

	/* The charge threshold is only reset when the system is power cycled,
	 * and we can't get the current threshold so let set it to 100% when
	 * a battery is added.
	 */
	asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, 100, NULL);
	charge_end_threshold = 100;

	return 0;
}

Linux Kernel 5.7 and above

BAT0 and BATT battery names supported. BATT support added via commit 6b3586d45bba14f6912f37488090c37a3710e7b4:

static int asus_wmi_battery_add(struct power_supply *battery)
{
	/* The WMI method does not provide a way to specific a battery, so we
	 * just assume it is the first battery.
	 * Note: On some newer ASUS laptops (Zenbook UM431DA), the primary/first
	 * battery is named BATT.
	 */
	if (strcmp(battery->desc->name, "BAT0") != 0 &&
	    strcmp(battery->desc->name, "BATT") != 0)
		return -ENODEV;

	if (device_create_file(&battery->dev,
	    &dev_attr_charge_control_end_threshold))
		return -ENODEV;

	/* The charge threshold is only reset when the system is power cycled,
	 * and we can't get the current threshold so let set it to 100% when
	 * a battery is added.
	 */
	asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, 100, NULL);
	charge_end_threshold = 100;

	return 0;
}

Linux Kernel 5.4 and above

Linux ACPI WMI driver/module for ASUS implements support for ASUS Battery Health Charging and BAT0 via commit d507a54f5865d8dcbdd16c66a1a2da15640878ca and commit 7973353e92ee1e7ca3b2eb361a4b7cb66c92abee

static int asus_wmi_battery_add(struct power_supply *battery)
{
	/* The WMI method does not provide a way to specific a battery, so we
	 * just assume it is the first battery.
	 */
	if (strcmp(battery->desc->name, "BAT0") != 0)
		return -ENODEV;

	if (device_create_file(&battery->dev,
	    &dev_attr_charge_control_end_threshold))
		return -ENODEV;

	/* The charge threshold is only reset when the system is power cycled,
	 * and we can't get the current threshold so let set it to 100% when
	 * a battery is added.
	 */
	asus_wmi_set_devstate(ASUS_WMI_DEVID_RSOC, 100, NULL);
	charge_end_threshold = 100;

	return 0;
}

References

Understanding Systemd Units and Unit Files | DigitalOcean
Increasingly, Linux distributions are adopting or planning to adopt the systemd init system. This powerful suite of software can manage many aspects of your …
Writing a WMI driver - an introduction
Windows Management Instrumentation (WMI) is a set of extensions to the Windows Driver Model that provides an operating system interface for dealing with platform devices. WMI objects can be embedded within ACPI, a configuration which Microsoft recommends. Like ACPI, WMI is not really standardized…
Kernel/Reference/WMI - Ubuntu Wiki
The Linux Kernel Archives
GitHub - torvalds/linux: Linux kernel source tree
Linux kernel source tree. Contribute to torvalds/linux development by creating an account on GitHub.
Battery Care — TLP 1.5 documentation
Laptop/ASUS - ArchWiki
ACPI battery and power subsystem firmware implementation
This topic details how the platform should expose power subsystem information to the Windows power manager.
Advanced Configuration and Power Interface (ACPI) BIOS - Windows drivers
ACPI BIOS
WMI Architecture - Win32 apps
WMI provides a uniform interface for any local or remote applications or scripts that obtain management data from a computer system, a network, or an enterprise.
WMI ACPI Sample - Code Samples
Contains ACPI BIOS and WMI sample code that enables instrumentation of the ACPI BIOS from within ACPI Source Language (ASL) code.