From nobody Fri Apr 3 04:34:27 2026 Received: from mail-dy1-f169.google.com (mail-dy1-f169.google.com [74.125.82.169]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 334FF3C5DD5 for ; Tue, 24 Mar 2026 22:10:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.169 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774390244; cv=none; b=X/1k4BVmNYVYH/oEjaApHa/3erh7n5W0Et9cslTuSTjXVXHTjP3OtuLTOxJ/2tsTj7kwI8QZVjdHf5JLf+oN0uZVY8QF2x40ODCL0oHtDA0uROHqbDZ/WYiBDCf+gw+dLHohBahFLEWa8PHXHztNbtkzYWiLT4QOX8yFue0A6YY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774390244; c=relaxed/simple; bh=++q/TIbFFULKGy6KkUZYgcdJioWrXHdR5t3T1e5fAao=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=HoOu9jD57vvi79GKyLPS/81ScXUF1QBWUBHP6ihCm5+niGQ932xwX+RgOYQdCis8v854PyAHeVQ2bahkRX0GsmLn2W/qRxGwPFdB0zy/T2qMRTzPRE1a0sYZjb9lGDYe5GJZOepdCjdXguIEcmkAWMzbyvqd4mcEwJh6yU+SX7g= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=knWSWGoh; arc=none smtp.client-ip=74.125.82.169 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="knWSWGoh" Received: by mail-dy1-f169.google.com with SMTP id 5a478bee46e88-2c160cb021cso116852eec.1 for ; Tue, 24 Mar 2026 15:10:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1774390241; x=1774995041; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=2gMM/Qe6t0JjJ7msgQKHqXE41UfA697h2w/oTgTV6Dw=; b=knWSWGoh65RegKv5wNW1RZEo2oZEpp8QWwccoYZPPqE+WOHo9az8dxWxEW41DP4UAa FhcIdj4I3pydtrW3G/hIgf9SpQdXRHciHoSRs/NWK7S9aXCqpdyiIV5HdZ3wFNQisnbZ rgvQvT3vS/f7ByOSURFhVdoALYuIsaD08p4+Uc1qquZ1GYS7v0bEtrUM70oW+rGvbveM hwmkndEZxy/iCQvlcT6NjJjJ4bEcrF7mJe5uwD1Whqyl6RTYGRrcLuUQPkds1xwUxZue LBg8yw7Nq4Vt9olsLA2HJ5lNNSopk5a+qud8sTXFrAnz9ETSuJsaj4CY2vtlGWRhyHcD iowA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1774390241; x=1774995041; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=2gMM/Qe6t0JjJ7msgQKHqXE41UfA697h2w/oTgTV6Dw=; b=SZAJmGll0j2PUCINvH4mt4O116ByLQ5MYChJEAgtoqwTmMaiEjLAQ3CfHo/TX13Nys MpyTbbHR1lvDfxW4ndzexKC4e0527TqcqeG7dTYJhvZfESgsFKXO4SC4Yf1UEVSqcKXy HpFMD+IC5AKLbTie2EVF+KhXjTedbJ9QUVgR7YzTWxcaZF7QjFDUYAYAs0g5AuaQGZ7p Pug85dAEKR5UH1TASvWTFLvxgd7eUTJkaBgSNOiL9ZuwsyeaMEbxo0NGj0heArfRX3/d TDzXIHx5j9Il6x8VWAA8CkaFdUAP5Os0tUPPoYsruS7MBXOqJCb7a533Xb4rBS/duU05 DZlw== X-Forwarded-Encrypted: i=1; AJvYcCVKGZ5+dAgtEEEXUsZ/GvIsbaQ840KZY1BK8xYDBeA3t8KE1DjFItYCTH6v3OEyO7mhLsAjIsbrqU+M7ug=@vger.kernel.org X-Gm-Message-State: AOJu0YwWCKdFzP9OtRsSyNrBzOdud6WU6Q32tmTA+pT/S99p74Xd4Dhj U/Ha91RLzO6UMixGUIebP/uZuKZkzzYHTnszPcA4fy3UcZ4ZGBhk1TWS X-Gm-Gg: ATEYQzxnMERP3PjfQNH9G0pwy69rg+EwS3xuOhi5GeF9w7AAeTCzUkb9igcMI1aHKgU eqhy7SXFbiP3ZyaZuODdw5b2kVfJvdgHi5eM3zdNtDsPTKSpGn1a96OZU6fkA46PPEBRyC1fMzD sNbH4O1KwrH8PVO9vftDr8+BBhxgZJFwngCmlh0umIGFq63+NkaYeJvUAdtDKJh15NwTgBCW/hR MHSzhiUfZ8DpbV47JHLBYKGW+sNyRHC/MsMalv1sWvZkirnlFH3R4njDwiKhs4dL2sirGGgvLDK +N3UasrxMoieaXbJo8FJ5yD92N9/5vW4PZztIt9drwqN8VyQqxq/+j4P+FS1hZT8PKznrotwZOM OV5Q7aHRqNuG+0nF9lWqTILKUxSdglG3uyEYdEj8OV4+R9lQD3s8YVBArB201i64txe+ybhBcoc SW6UEhDKC8QCvUzyGD3ioUREnpZtfa6bn/w4TnugMqolTKyng0gWKNgXRVB+hVnUj3LjtcsEilb QNu X-Received: by 2002:a05:7301:4088:b0:2c1:5134:54d3 with SMTP id 5a478bee46e88-2c15d433488mr550753eec.22.1774390240958; Tue, 24 Mar 2026 15:10:40 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-2c10b14c299sm16282832eec.6.2026.03.24.15.10.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 24 Mar 2026 15:10:40 -0700 (PDT) From: "Derek J. Clark" To: =?UTF-8?q?Ilpo=20J=C3=A4rvinen?= , Hans de Goede Cc: Mark Pearson , Armin Wolf , Jonathan Corbet , Rong Zhang , Kurt Borja , "Derek J . Clark" , platform-driver-x86@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v5 8/8] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting Date: Tue, 24 Mar 2026 22:10:32 +0000 Message-ID: <20260324221032.1333636-9-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260324221032.1333636-1-derekjohn.clark@gmail.com> References: <20260324221032.1333636-1-derekjohn.clark@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 Signed-off-by: Derek J. Clark --- v5: - Use switch statement instead of if for battery charge state set/get. - use force_load_psy_ext to skip all ACPI interactions. - Various formatting fixes. 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/Kconfig | 1 + drivers/platform/x86/lenovo/wmi-capdata.h | 1 + drivers/platform/x86/lenovo/wmi-other.c | 250 +++++++++++++++++++++- 3 files changed, 251 insertions(+), 1 deletion(-) diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/len= ovo/Kconfig index f885127b007f..75a8b144b0da 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -263,6 +263,7 @@ config LENOVO_WMI_GAMEZONE config LENOVO_WMI_TUNING tristate "Lenovo Other Mode WMI Driver" depends on ACPI_WMI + depends on ACPI_BATTERY select HWMON select FW_ATTR_CLASS select LENOVO_WMI_CAPDATA diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x= 86/lenovo/wmi-capdata.h index b026ee30c828..1939401c6c14 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 =3D 0x01, LWMI_DEVICE_ID_GPU =3D 0x02, + LWMI_DEVICE_ID_PSU =3D 0x03, LWMI_DEVICE_ID_FAN =3D 0x04, }; =20 diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86= /lenovo/wmi-other.c index 11c16857ef97..cef51cded7be 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -43,9 +43,12 @@ #include #include #include +#include #include #include =20 +#include + #include "wmi-capdata.h" #include "wmi-events.h" #include "wmi-gamezone.h" @@ -79,9 +82,11 @@ enum lwmi_feature_id_gpu { LWMI_FEATURE_ID_GPU_NV_CPU_BOOST =3D 0x0b, }; =20 -#define LWMI_FEATURE_ID_FAN_RPM 0x03 +#define LWMI_FEATURE_ID_FAN_RPM 0x03 +#define LWMI_FEATURE_ID_PSU_CHARGE_TYPE 0x01 =20 #define LWMI_TYPE_ID_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 =20 #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 @@ -92,10 +97,17 @@ enum lwmi_feature_id_gpu { =20 #define LWMI_FAN_DIV 100 =20 +#define LWMI_CHARGE_TYPE_STANDARD 0x00 +#define LWMI_CHARGE_TYPE_LONGLIFE 0x01 + #define LWMI_ATTR_ID_FAN_RPM(x) \ lwmi_attr_id(LWMI_DEVICE_ID_FAN, LWMI_FEATURE_ID_FAN_RPM, \ LWMI_GZ_THERMAL_MODE_NONE, LWMI_FAN_ID(x)) =20 +#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" =20 @@ -137,6 +149,8 @@ struct lwmi_om_priv { bool capdata00_collected : 1; bool capdata_fan_collected : 1; } fan_flags; + + struct acpi_battery_hook battery_hook; }; =20 /* @@ -561,6 +575,239 @@ static void lwmi_om_fan_info_collect_cd_fan(struct de= vice *dev, struct cd_list * lwmi_om_hwmon_add(priv); } =20 +/* =3D=3D=3D=3D=3D=3D=3D=3D Power Supply Extension (component: lenovo-wmi-= capdata 00) =3D=3D=3D=3D=3D=3D=3D=3D */ + +/** + * 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 =3D ext_data; + struct wmi_method_args_32 args; + u32 retval; + int ret; + + args.arg0 =3D LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE= _ID_PSU_AC); + + ret =3D 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", retva= l, prop); + + switch (retval) { + case LWMI_CHARGE_TYPE_LONGLIFE: + val->intval =3D POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + break; + case LWMI_CHARGE_TYPE_STANDARD: + val->intval =3D POWER_SUPPLY_CHARGE_TYPE_STANDARD; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge value: %#x\n", retval); + return -EINVAL; + } + + 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 =3D ext_data; + struct wmi_method_args_32 args; + + args.arg0 =3D LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE= _ID_PSU_AC); + switch (val->intval) { + case POWER_SUPPLY_CHARGE_TYPE_LONGLIFE: + args.arg1 =3D LWMI_CHARGE_TYPE_LONGLIFE; + break; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + args.arg1 =3D LWMI_CHARGE_TYPE_STANDARD; + break; + default: + dev_err(&priv->wdev->dev, "Got invalid charge value: %#x\n", val->intval= ); + return -EINVAL; + } + + dev_dbg(&priv->wdev->dev, "Attempting to set %#10x 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 =3D ext_data; + struct capdata00 capdata; + u32 attribute_id =3D LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LW= MI_TYPE_ID_PSU_AC); + int ret; + + ret =3D lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return false; + + dev_dbg(&priv->wdev->dev, "Battery charge mode (%#10x) support level: %#x= \n", + attribute_id, capdata.supported); + + return capdata.supported; +} + +static const enum power_supply_property lwmi_psy_ext_props[] =3D { + POWER_SUPPLY_PROP_CHARGE_TYPES, +}; + +static const struct power_supply_ext lwmi_psy_ext =3D { + .name =3D LWMI_OM_SYSFS_NAME, + .properties =3D lwmi_psy_ext_props, + .num_properties =3D ARRAY_SIZE(lwmi_psy_ext_props), + .charge_types =3D (BIT(POWER_SUPPLY_CHARGE_TYPE_STANDARD) | + BIT(POWER_SUPPLY_CHARGE_TYPE_LONGLIFE)), + .get_property =3D lwmi_psy_ext_get_prop, + .set_property =3D lwmi_psy_ext_set_prop, + .property_is_writeable =3D 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_batt= ery_hook *hook) +{ + struct lwmi_om_priv *priv =3D container_of(hook, struct lwmi_om_priv, bat= tery_hook); + + return power_supply_register_extension(battery, &lwmi_psy_ext, &priv->wde= v->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_b= attery_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 firs= t, + * 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 =3D context; + + if (!handle) + return AE_OK; + + *ahand =3D 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 conflic= t " + "with adding an extension to set the battery charge type. It is recommend= ed " + "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 firs= t, + * 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 =3D "VPC2004"; + acpi_handle handle =3D NULL; + int ret; + + /* Deconflict ideapad_laptop driver */ + if (force_load_psy_ext) + goto load_psy_ext; + + ret =3D acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); + if (ret) + return; + + if (handle && acpi_has_method(handle, "GBMD") && acpi_has_method(handle, = "SBMC")) { + dev_dbg(&priv->wdev->dev, "ideapad_laptop driver manages battery for dev= ice.\n"); + return; + } + +load_psy_ext: + /* Add battery hooks */ + priv->battery_hook.add_battery =3D lwmi_add_battery; + priv->battery_hook.remove_battery =3D lwmi_remove_battery; + priv->battery_hook.name =3D "Lenovo WMI Other Battery Extension"; + + ret =3D devm_battery_hook_register(&priv->wdev->dev, &priv->battery_hook); + if (ret) + dev_err(&priv->wdev->dev, "Error during battery hook: %i\n", ret); +} + /* =3D=3D=3D=3D=3D=3D=3D=3D fw_attributes (component: lenovo-wmi-capdata 0= 1) =3D=3D=3D=3D=3D=3D=3D=3D */ =20 struct tunable_attr_01 { @@ -1318,6 +1565,7 @@ static int lwmi_om_master_bind(struct device *dev) return -ENODEV; =20 lwmi_om_fan_info_collect_cd00(priv); + lwmi_om_ps_ext_init(priv); =20 return lwmi_om_fw_attr_add(priv); } --=20 2.53.0