Re: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
From: Rong Zhang
Date: Sun Mar 15 2026 - 15:05:24 EST
Hi Derek,
On Sat, 2026-03-14 at 20:52 -0700, Derek J. Clark wrote:
> On March 14, 2026 7:00:20 PM PDT, Rong Zhang <i@xxxxxxxx> wrote:
> > Hi Derek,
> >
> > On Thu, 2026-03-12 at 03:10 +0000, Derek J. Clark wrote:
> > > Add charge-type power supply extension for devices that support WMI based
> > > charge enable/disable.
> > >
> > > Lenovo Legion devices that implement WMI function and capdata ID
> > > 0x03010001 in their BIOS are able to enable or disable charging at 80%
> > > through the lenovo-wmi-other interface. Add a charge_type power supply
> > > extension to expose this capability to the sysfs.
> > >
> > > The ideapad_laptop driver can also provide the charge_type attribute. To
> > > avoid conflicts between the drivers, get the acpi_handle and do the same
> > > check that ideapad_laptop does when it enables the feature. If the
> > > feature is supported in ideapad_laptop, abort adding the extension from
> > > lenovo-wmi-other. The ACPI method is more reliable when both are
> > > present, from my testing, so we can prefer that implementation and do
> > > not need to worry about de-conflicting from inside that driver. A new
> > > module parameter, force_load_psy_ext, is provided to bypass this ACPI
> > > check, if desired.
> > >
> > > Reviewed-by: Mark Pearson <mpearson-lenovo@xxxxxxxxx>
> > > Signed-off-by: Derek J. Clark <derekjohn.clark@xxxxxxxxx>
> > > ---
> > > v4:
> > > - Remove unused defines.
> > > - Disambiguate charging defines by renaming them to be more consistent
> > > with the kernel modes they represent.
> > > - Add module parameter to ignore ACPI checks.
> > > - Don't fail if the ACPI handle isn't found, skip the ACPI check
> > > instead.
> > > ---
> > > drivers/platform/x86/lenovo/wmi-capdata.h | 1 +
> > > drivers/platform/x86/lenovo/wmi-other.c | 234 +++++++++++++++++++++-
> > > 2 files changed, 234 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> > > index 196481b1ce17..cc0d1c176c2f 100644
> > > --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> > > +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> > > @@ -20,6 +20,7 @@
> > > enum lwmi_device_id {
> > > LWMI_DEVICE_ID_CPU = 0x01,
> > > LWMI_DEVICE_ID_GPU = 0x02,
> > > + LWMI_DEVICE_ID_PSU = 0x03,
> > > LWMI_DEVICE_ID_FAN = 0x04,
> > > };
> > >
> > > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > > index 61ac9bee352f..cab9ae6bd811 100644
> > > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > > @@ -43,9 +43,12 @@
> > > #include <linux/module.h>
> > > #include <linux/notifier.h>
> > > #include <linux/platform_profile.h>
> > > +#include <linux/power_supply.h>
> > > #include <linux/types.h>
> > > #include <linux/wmi.h>
> > >
> > > +#include <acpi/battery.h>
> > > +
> > > #include "wmi-capdata.h"
> > > #include "wmi-events.h"
> > > #include "wmi-gamezone.h"
> > > @@ -79,10 +82,12 @@ enum lwmi_feature_id_gpu {
> > > LWMI_FEATURE_ID_GPU_NV_CPU_BOOST = 0x0b,
> > > };
> > >
> > > -#define LWMI_FEATURE_ID_FAN_RPM 0x03
> > > +#define LWMI_FEATURE_ID_FAN_RPM 0x03
> > > +#define LWMI_FEATURE_ID_PSU_CHARGE_TYPE 0x01
> > >
> > > #define LWMI_TYPE_ID_NONE 0x00
> > > #define LWMI_TYPE_ID_CROSSLOAD 0x01
> > > +#define LWMI_TYPE_ID_PSU_AC 0x01
> > >
> > > #define LWMI_FEATURE_VALUE_GET 17
> > > #define LWMI_FEATURE_VALUE_SET 18
> > > @@ -93,6 +98,9 @@ enum lwmi_feature_id_gpu {
> > >
> > > #define LWMI_FAN_DIV 100
> > >
> > > +#define LWMI_CHARGE_TYPE_STANDARD 0x00
> > > +#define LWMI_CHARGE_TYPE_LONGLIFE 0x01
> > > +
> > > /**
> > > * lwmi_attr_id() - Formats a capability data attribute ID
> > > * @dev_id: The u8 corresponding to the device ID.
> > > @@ -114,6 +122,10 @@ static u32 lwmi_attr_id(u8 dev_id, u8 feat_id, u8 mode_id, u8 type_id)
> > > lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \
> > > LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x))
> > >
> > > +#define LWMI_ATTR_ID_PSU(feat, type) \
> > > + lwmi_attr_id(LWMI_DEVICE_ID_PSU, feat, \
> > > + LWMI_GZ_THERMAL_MODE_NONE, type)
> > > +
> > > #define LWMI_OM_SYSFS_NAME "lenovo-wmi-other"
> > > #define LWMI_OM_HWMON_NAME "lenovo_wmi_other"
> > >
> > > @@ -155,6 +167,8 @@ struct lwmi_om_priv {
> > > bool capdata00_collected : 1;
> > > bool capdata_fan_collected : 1;
> > > } fan_flags;
> > > +
> > > + struct acpi_battery_hook battery_hook;
> > > };
> > >
> > > /*
> > > @@ -579,6 +593,223 @@ static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *
> > > lwmi_om_hwmon_add(priv);
> > > }
> > >
> > > +/* ======== Power Supply Extension (component: lenovo-wmi-capdata 00) ======== */
> > > +
> > > +/**
> > > + * lwmi_psy_ext_get_prop() - Get a power_supply_ext property
> > > + * @ps: The battery that was extended
> > > + * @ext: The extension
> > > + * @ext_data: Pointer the lwmi_om_priv drvdata
> > > + * @prop: The property to read
> > > + * @val: The value to return
> > > + *
> > > + * Writes the given value to the power_supply_ext property
> > > + *
> > > + * Return: 0 on success, or an error
> > > + */
> > > +static int lwmi_psy_ext_get_prop(struct power_supply *ps,
> > > + const struct power_supply_ext *ext,
> > > + void *ext_data,
> > > + enum power_supply_property prop,
> > > + union power_supply_propval *val)
> > > +{
> > > + struct lwmi_om_priv *priv = ext_data;
> > > + struct wmi_method_args_32 args;
> > > + u32 retval;
> > > + int ret;
> > > +
> > > + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE_ID_PSU_AC);
> > > +
> > > + ret = lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_GET,
> > > + (unsigned char *)&args, sizeof(args),
> > > + &retval);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dev_dbg(&priv->wdev->dev, "Got return value %x for property %x\n", retval, prop);
> >
> > %#x
> >
> > > +
> > > + if (retval == LWMI_CHARGE_TYPE_LONGLIFE)
> > > + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE;
> > > + else
> > > + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD;
> >
> > I'd suggest using a switch statement here with a default path to
> > dev_err() unexpected retval.
> >
>
> I'll give it a shot. I recall there being some unexplained issues when I tried that originally, though I can't recall the specifics other than it seemed to trigger false negative outcomes for some reason. I'll let you know if I run into it again with more details so we can agree on a way forward if it becomes a problem.
IMO we should be cautious while handling the returned value from
firmware, as we never know how broken it could be. Treating
POWER_SUPPLY_CHARGE_TYPE_STANDARD as a wildcard is a bad idea.
If there are some false negatives, we should handle them explicitly
instead of blindly mapping them to POWER_SUPPLY_CHARGE_TYPE_STANDARD.
Thanks,
Rong
>
> Thanks again,
> - Derek
>
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +/**
> > > + * lwmi_psy_ext_set_prop() - Set a power_supply_ext property
> > > + * @ps: The battery that was extended
> > > + * @ext: The extension
> > > + * @ext_data: Pointer the lwmi_om_priv drvdata
> > > + * @prop: The property to write
> > > + * @val: The value to write
> > > + *
> > > + * Writes the given value to the power_supply_ext property
> > > + *
> > > + * Return: 0 on success, or an error
> > > + */
> > > +static int lwmi_psy_ext_set_prop(struct power_supply *ps,
> > > + const struct power_supply_ext *ext,
> > > + void *ext_data,
> > > + enum power_supply_property prop,
> > > + const union power_supply_propval *val)
> > > +{
> > > + struct lwmi_om_priv *priv = ext_data;
> > > + struct wmi_method_args_32 args;
> > > +
> > > + args.arg0 = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE_ID_PSU_AC);
> > > + if (val->intval == POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)
> > > + args.arg1 = LWMI_CHARGE_TYPE_LONGLIFE;
> > > + else
> > > + args.arg1 = LWMI_CHARGE_TYPE_STANDARD;
> >
> > Ditto.
> >
> > > +
> > > + dev_dbg(&priv->wdev->dev, "Attempting to set %#08x for property %x to %x\n",
> >
> > %#010x and %#x
> >
> > > + args.arg0, prop, args.arg1);
> > > +
> > > + return lwmi_dev_evaluate_int(priv->wdev, 0x0, LWMI_FEATURE_VALUE_SET,
> > > + (unsigned char *)&args, sizeof(args), NULL);
> > > +}
> > > +
> > > +/**
> > > + * lwmi_psy_prop_is_writeable() - Determine if the property is supported
> > > + * @ps: The battery that was extended
> > > + * @ext: The extension
> > > + * @ext_data: Pointer the lwmi_om_priv drvdata
> > > + * @prop: The property to check
> > > + *
> > > + * Checks capdata 00 to determine if the property is supported.
> > > + *
> > > + * Return: Support level, or false
> > > + */
> > > +static int lwmi_psy_prop_is_writeable(struct power_supply *ps,
> > > + const struct power_supply_ext *ext,
> > > + void *ext_data,
> > > + enum power_supply_property prop)
> > > +{
> > > + struct lwmi_om_priv *priv = ext_data;
> > > + struct capdata00 capdata;
> > > + u32 attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE_ID_PSU_AC);
> > > + int ret;
> > > +
> > > + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata);
> > > + if (ret)
> > > + return false;
> > > +
> > > + dev_dbg(&priv->wdev->dev, "Battery charge mode (%#08x) support level: %x\n",
> >
> > Ditto.
> >
> > > + attribute_id, capdata.supported);
> > > +
> > > + return capdata.supported;
> > > +}
> > > +
> > > +static const enum power_supply_property lwmi_psy_ext_props[] = {
> > > + POWER_SUPPLY_PROP_CHARGE_TYPES,
> > > +};
> > > +
> > > +static const struct power_supply_ext lwmi_psy_ext = {
> > > + .name = LWMI_OM_SYSFS_NAME,
> > > + .properties = lwmi_psy_ext_props,
> > > + .num_properties = ARRAY_SIZE(lwmi_psy_ext_props),
> > > + .charge_types = (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) |
> > > + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)),
> > > + .get_property = lwmi_psy_ext_get_prop,
> > > + .set_property = lwmi_psy_ext_set_prop,
> > > + .property_is_writeable = lwmi_psy_prop_is_writeable,
> > > +};
> >
> > Update Kconfig with `depends on ACPI_BATTERY' like other drivers did.
> >
> > > +
> > > +/**
> > > + * lwmi_add_battery() - Connect the power_supply_ext
> > > + * @battery: The battery to extend
> > > + * @hook: The driver hook used to extend the battery
> > > + *
> > > + * Return: 0 on success, or an error.
> > > + */
> > > +static int lwmi_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
> > > +{
> > > + struct lwmi_om_priv *priv = container_of(hook, struct lwmi_om_priv, battery_hook);
> > > +
> > > + return power_supply_register_extension(battery, &lwmi_psy_ext, &priv->wdev->dev, priv);
> > > +}
> > > +
> > > +/**
> > > + * lwmi_remove_battery() - Disconnect the power_supply_ext
> > > + * @battery: The battery that was extended
> > > + * @hook: The driver hook used to extend the battery
> > > + *
> > > + * Return: 0 on success, or an error.
> > > + */
> > > +static int lwmi_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
> > > +{
> > > + power_supply_unregister_extension(battery, &lwmi_psy_ext);
> > > + return 0;
> > > +}
> > > +
> > > +/**
> > > + * lwmi_acpi_match() - Attempts to return the ideapad acpi handle
> > > + * @handle: The ACPI handle that manages battery charging
> > > + * @lvl: Unused
> > > + * @context: Void pointer to the acpi_handle object to return
> > > + * @retval: Unused
> > > + *
> > > + * Checks if the ideapad_laptop driver is going to manage charge_type first,
> > > + * then if not, hooks the battery to our WMI methods.
> > > + *
> > > + * Return: AE_CTRL_TERMINATE if found, AE_OK if not found.
> > > + */
> > > +static acpi_status lwmi_acpi_match(acpi_handle handle, u32 lvl,
> > > + void *context, void **retval)
> > > +{
> > > + acpi_handle *ahand = context;
> > > +
> > > + if (!handle)
> > > + return AE_OK;
> > > +
> > > + *ahand = handle;
> > > +
> > > + return AE_CTRL_TERMINATE;
> > > +}
> > > +
> > > +static bool force_load_psy_ext;
> > > +module_param(force_load_psy_ext, bool, 0444);
> > > +MODULE_PARM_DESC(force_load_psy_ext,
> > > + "This option will skip checking if the ideapad_laptop driver will conflict "
> > > + "with adding an extension to set the battery charge type. It is recommended "
> > > + "to blacklist the ideapad driver before using this option.");
> > > +
> > > +/**
> > > + * lwmi_om_ps_ext_init() - Hooks power supply extension to device battery
> > > + * @priv: Driver private data
> > > + *
> > > + * Checks if the ideapad_laptop driver is going to manage charge_type first,
> > > + * then if not, hooks the battery to our WMI methods.
> > > + */
> > > +static void lwmi_om_ps_ext_init(struct lwmi_om_priv *priv)
> > > +{
> > > + static const char * const ideapad_hid = "VPC2004";
> > > + acpi_handle handle = NULL;
> > > + int ret;
> > > +
> > > + /* Deconflict ideapad_laptop driver */
> > > + ret = acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL);
> >
> > Skip it in the force_load_psy_ext=Y case.
> >
> > > + if (ret)
> > > + return;
> > > +
> > > + if (handle && !force_load_psy_ext) {
> > > + if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) {
> >
> > Connect them with `&&' to prevent unnecessary indents and...
> >
> > > + dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for device.\n");
> >
> > ...line length exceeding 100.
> >
> > Thanks,
> > Rong
> >
> > > + return;
> > > + }
> > > + }
> > > +
> > > + /* Add battery hooks */
> > > + priv->battery_hook.add_battery = lwmi_add_battery;
> > > + priv->battery_hook.remove_battery = lwmi_remove_battery;
> > > + priv->battery_hook.name = "Lenovo WMI Other Battery Extension";
> > > +
> > > + ret = devm_battery_hook_register(&priv->wdev->dev, &priv->battery_hook);
> > > + if (ret)
> > > + dev_err(&priv->wdev->dev, "Error during battery hook: %i\n", ret);
> > > +}
> > > +
> > > /* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */
> > >
> > > struct tunable_attr_01 {
> > > @@ -1334,6 +1565,7 @@ static int lwmi_om_master_bind(struct device *dev)
> > > return -ENODEV;
> > >
> > > lwmi_om_fan_info_collect_cd00(priv);
> > > + lwmi_om_ps_ext_init(priv);
> > >
> > > return lwmi_om_fw_attr_add(priv);
> > > }