From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-dl1-f52.google.com (mail-dl1-f52.google.com [74.125.82.52]) (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 34976386443 for ; Sun, 12 Apr 2026 21:11:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.52 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776028297; cv=none; b=fs5+EpaveqOHjnZEb8ig+D8kPZztjtLfvmtQpsTus1Wv9MEdF9bQTG00NBFQI6BXZy5V2mDxSDn6DJt2Uc3wFTfbPu0D7TRtMR0BQwPXJUkp4gQsqKi5sG/hJoHzulMG6U6MAFkmTIleiaJkBVi2TK6ucTdLnCjOv6Zmx19348I= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776028297; c=relaxed/simple; bh=zmRAhMCgRYWlFKNoPMMBWDBeg7moDqKzkQS6CAEp3MI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=gQZCdd6EPw9/WDGZwGD0YknTKoc2Wuh647WwIKj2QWFsCLm2AWsNGZGAzG8NTo4WxUjTNy1tl8svzmmB0vk+aRV36Tp0IM+Wwo9j/B7dekKsdypMzMmG0Lz4N4dPc6G3+JRXNSGsn21aLtaIVvwE3Wxo+Pt6+62DrBJnlC8jNiw= 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=Uj7226oA; arc=none smtp.client-ip=74.125.82.52 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="Uj7226oA" Received: by mail-dl1-f52.google.com with SMTP id a92af1059eb24-12c3d30564eso2831718c88.1 for ; Sun, 12 Apr 2026 14:11:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1776028293; x=1776633093; 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=vwZokcEODQRhlAhcUfPMvItSavV6p3ptGFjauBi5XnA=; b=Uj7226oAjlZDkutieUDw7EZRPWsRkZMRCjl1dA//asfGSIcP4mpNBVze2ILxEOdqGK Ij54fpxnR7DttXHZektqhzG3emcgGwsIdRluHBx5hhCThWk+nLINJto8yDZBH1tGbHQ6 JlLpH4t0/CpexboySnt17QrDfR9uzqvhIW7SRVhRymCxuStRz1r1DcJD1KiCSP9+9i9r wjkV1/3OUhA38kNxDq/OsejkXr7WEmYGbEZcLxmpyAlOyT0N/XK/iCrJymchwavD6vHE ERXqo9OnjJ8HQ5a+MkmQ8NUdCR2vGGk26rb58xZXt+fdrYwNEba4eptL92nVa9YRsKXw e+Dw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776028293; x=1776633093; 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=vwZokcEODQRhlAhcUfPMvItSavV6p3ptGFjauBi5XnA=; b=FnMkCD6oxEwtsZDJHykkUF15tKE2IMEUl6lhI6JINzJnEYxcxhbOnfjP2TnL9pbuGm jSwuUdSv823Q0EkU8SMYelcAeMAYaNibcyYGWrZxH1kPbCGyQKhEmEyF5hVd+SWhyGw0 wYcatp9dIsVGALFRaGRdlNtyqKG++pqV8Wuj+w2+IM/e0luQc8K4sO0yzt4WdK0yzWOL KB4HgJ1M3gK//DvKJuPZt4nNbyMO/W3wv7MTwx6B0JX2b1V3QnYOqooRrxUdRQjtmWkh z8QQcrZzoHY1J+K6XcfstV9Wo4DxjdVh6U/Xlp1dmWzQaTTUsI1eEVSv+yTUcXJcocta fIRA== X-Forwarded-Encrypted: i=1; AJvYcCVgLRE1fFh4xCkE/1YJVuheiC47EODdPx9EQQ/4IvGpt4lRQ95K5u5PFFVZ9wIO/4+Z0+GMNhMflVxzU/k=@vger.kernel.org X-Gm-Message-State: AOJu0YwJE9bNF8RVvwgZhDnVCjMJiqWIzYX1ZaAcxBL8NX83DZYjN4ic Ap3S9p/vXlIv7Ryf29gwznlC9D19OTJtfe9LGKF/DJyw86yWk1E0ezUK X-Gm-Gg: AeBDiesGJQyW//E92RuLCYi/OrQQmtEnBuTh0M+R9ZtSHBvY5NZnV4LuQZWEeHjpvwO 7KUsa9TBjX891/LBzyaahnfIn9rUOZVkmzLDCB+JXXFfpVBl5APsgvquvFIWyXlc95O7ib9nswg RTdNeSoRoDWAbz5EOyu2KSQE/GZUM5T7wGYp/4wmuPFpdHYkSMIjsKarKppzzCZWn/3LX0+3jUp N8YMN0gJgnlio0yRlPuyDv/OSCTzM8/OqZ5tItDcBNXOjV/Jm58/XYbDEdZeLifKaEgr1u/6Yx+ M3M+pZZS+R6o2RC/0i7S8l/N9VhRZg972Jzfulf2y7Nd+EIFknCp5arxVXzQNQ4REhNtcOUjFIX g5jurKpBNZJb+a4svrdgnHmKM20hNoCOiP25g3mFefqWfIsQ8i/YTGr0wHaAMMEUyIpY+Uz5iSk epevMemptnc7pFByx9ZaQ9rzHtRa/vedJTNlV4yK46aeCnv75wDbg82aZjk5LVo0Ppy9FpVpjkO 7pk X-Received: by 2002:a05:7022:4a4:b0:12b:ebb9:1c18 with SMTP id a92af1059eb24-12c34efabadmr6852818c88.31.1776028293308; Sun, 12 Apr 2026 14:11:33 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-12c346fb141sm11520856c88.12.2026.04.12.14.11.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Apr 2026 14:11:33 -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 v10 14/16] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting Date: Sun, 12 Apr 2026 14:11:19 -0700 Message-ID: <20260412211121.2220556-15-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260412211121.2220556-1-derekjohn.clark@gmail.com> References: <20260412211121.2220556-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: 8bit 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: Rong Zhang Reviewed-by: Mark Pearson Signed-off-by: Derek J. Clark --- v7: - Use devm_battery_hook_register, manually unregister during unbind. v6: - Check feature flags to determine if the extension should be loaded and if it is writable. - Zero initialize wmi_method_args_32. - Fix formatting. 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 | 293 +++++++++++++++++++++- 3 files changed, 294 insertions(+), 1 deletion(-) diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig index 09b1b055d2e0..b9a5d18caa1e 100644 --- a/drivers/platform/x86/lenovo/Kconfig +++ b/drivers/platform/x86/lenovo/Kconfig @@ -262,6 +262,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/x86/lenovo/wmi-capdata.h index 891b12ca1db6..e0a30f2c0c87 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -21,6 +21,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 9636a3dc0788..712c11b69b45 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -40,9 +40,12 @@ #include #include #include +#include #include #include +#include + #include "wmi-capdata.h" #include "wmi-events.h" #include "wmi-helpers.h" @@ -74,9 +77,11 @@ 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_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 @@ -87,10 +92,17 @@ enum lwmi_feature_id_gpu { #define LWMI_FAN_DIV 100 +#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)) +#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" @@ -130,6 +142,9 @@ struct lwmi_om_priv { bool capdata00_collected : 1; bool capdata_fan_collected : 1; } fan_flags; + + struct acpi_battery_hook battery_hook; + bool bh_registered; }; /* @@ -554,6 +569,279 @@ 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 wmi_method_args_32 args = {}; + struct lwmi_om_priv *priv = ext_data; + 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); + + switch (retval) { + case LWMI_CHARGE_TYPE_LONGLIFE: + val->intval = POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + break; + case LWMI_CHARGE_TYPE_STANDARD: + val->intval = 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 wmi_method_args_32 args = {}; + struct lwmi_om_priv *priv = ext_data; + + args.arg0 = 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 = LWMI_CHARGE_TYPE_LONGLIFE; + break; + case POWER_SUPPLY_CHARGE_TYPE_STANDARD: + args.arg1 = 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 %#010x 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_supported() - Determine if the property is supported + * @priv: Pointer the lwmi_om_priv drvdata + * + * Checks capdata 00 to determine if the property is supported. + * + * Return: true if readable, or false + */ +static bool lwmi_psy_prop_is_supported(struct lwmi_om_priv *priv) +{ + u32 attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE_ID_PSU_AC); + struct capdata00 capdata; + 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 (%#010x) support level: %#x\n", + attribute_id, capdata.supported); + + return ((capdata.supported & LWMI_SUPP_VALID) && (capdata.supported & LWMI_SUPP_GET)); +} + +/** + * lwmi_psy_prop_is_writeable() - Determine if the property is writeable + * @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 writable. + * + * Return: true if writable, 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) +{ + u32 attribute_id = LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_CHARGE_TYPE, LWMI_TYPE_ID_PSU_AC); + struct lwmi_om_priv *priv = ext_data; + struct capdata00 capdata; + int ret; + + ret = lwmi_cd00_get_data(priv->cd00_list, attribute_id, &capdata); + if (ret) + return false; + + return !!(capdata.supported & LWMI_SUPP_SET); +} + +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_psy_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_psy_ext_init(struct lwmi_om_priv *priv) +{ + static const char * const ideapad_hid = "VPC2004"; + acpi_handle handle = NULL; + int ret; + + priv->bh_registered = false; + + /* Deconflict ideapad_laptop driver */ + if (force_load_psy_ext) + goto load_psy_ext; + + if (!lwmi_psy_prop_is_supported(priv)) + return; + + ret = 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 device\n"); + return; + } + +load_psy_ext: + /* 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"; + priv->bh_registered = true; + + battery_hook_register(&priv->battery_hook); +} + +/** + * lwmi_om_psy_remove() - Unregister battery hook + * @priv: Driver private data + * + * Unregisters the battery hook if applicable. + */ +static void lwmi_om_psy_remove(struct lwmi_om_priv *priv) +{ + if (!priv->bh_registered) + return; + + battery_hook_unregister(&priv->battery_hook); + priv->bh_registered = false; +} + /* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */ struct tunable_attr_01 { @@ -1228,6 +1516,7 @@ static int lwmi_om_master_bind(struct device *dev) } lwmi_om_fan_info_collect_cd00(priv); + lwmi_om_psy_ext_init(priv); lwmi_om_fw_attr_add(priv); @@ -1250,6 +1539,8 @@ static void lwmi_om_master_unbind(struct device *dev) lwmi_om_hwmon_remove(priv); + lwmi_om_psy_remove(priv); + component_unbind_all(dev, NULL); } -- 2.53.0