[PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting

Derek J. Clark posted 8 patches 3 weeks, 5 days ago
There is a newer version of this series
[PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
Posted by Derek J. Clark 3 weeks, 5 days ago
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@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
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);
+
+	if (retval == LWMI_CHARGE_TYPE_LONGLIFE)
+		val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE;
+	else
+		val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD;
+
+	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;
+
+	dev_dbg(&priv->wdev->dev, "Attempting to set %#08x for property %x to %x\n",
+		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",
+		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,
+};
+
+/**
+ * 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);
+	if (ret)
+		return;
+
+	if (handle && !force_load_psy_ext) {
+		if (acpi_has_method(handle, "GBMD") && acpi_has_method(handle, "SBMC")) {
+			dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for device.\n");
+			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);
 }
-- 
2.53.0
Re: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
Posted by kernel test robot 2 weeks, 6 days ago
Hi Derek,

kernel test robot noticed the following build errors:

[auto build test ERROR on linus/master]
[also build test ERROR on v7.0-rc4 next-20260317]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Derek-J-Clark/platform-x86-lenovo-wmi-other-Move-LWMI_FAN_DIV/20260312-111508
base:   linus/master
patch link:    https://lore.kernel.org/r/20260312031032.3467565-9-derekjohn.clark%40gmail.com
patch subject: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
config: x86_64-randconfig-003-20260317 (https://download.01.org/0day-ci/archive/20260318/202603180742.oHHUR1NX-lkp@intel.com/config)
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260318/202603180742.oHHUR1NX-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202603180742.oHHUR1NX-lkp@intel.com/

All errors (new ones prefixed by >>, old ones prefixed by <<):

>> ERROR: modpost: "devm_battery_hook_register" [drivers/platform/x86/lenovo/lenovo-wmi-other.ko] undefined!

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
Re: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
Posted by Rong Zhang 3 weeks, 2 days ago
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@squebb.ca>
> Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
> ---
> 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.

> +
> +	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);
>  }
Re: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
Posted by Derek J. Clark 3 weeks, 2 days ago
On March 14, 2026 7:00:20 PM PDT, Rong Zhang <i@rong.moe> 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@squebb.ca>
>> Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
>> ---
>> 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.

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);
>>  }
Re: [PATCH v4 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting
Posted by Rong Zhang 3 weeks, 1 day ago
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@rong.moe> 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@squebb.ca>
> > > Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > > ---
> > > 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);
> > >  }