From nobody Sun Apr 5 21:13:06 2026 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 ADC262836A6 for ; Tue, 24 Feb 2026 04:32:11 +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=1771907533; cv=none; b=g4g8prTvq5Zlmlshr9o8eA2c6ut0YTIba6s+OJA0G8GIjMTzg9Z0HIqMIlZ5LKNWc2rLjMhY8bw4N95zMmy1d6NmUVHsTh8F40GpTOX0+1bMPruaAksEEF9g1s46zUir1/pSIrbzIgxLaKUlmqAHDMtusrpccaZh6KN44GQdJNU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1771907533; c=relaxed/simple; bh=kessVQ8OOCDF+bsFY6W3zPIUXmUylLTWeH7nV4KNA9Q=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=GIivO7+PZDX3n4B24lr85J2eV2/i0Z5Iz4OyrtJVfXZJu1YehlqNMYYBL+9zLvIrZBmkjBZCJ08FRugXnVZx+qdTX+wbTVNMWxBcdhcdyfHxFqZ0il7uKjKs+3w61NTlk8JbRqpsM/hLzAaJRpdEx3evk0d5A+9x/tRtDjZqqf8= 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=V4tYjiiX; 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="V4tYjiiX" Received: by mail-dl1-f52.google.com with SMTP id a92af1059eb24-127423bea4bso34537c88.0 for ; Mon, 23 Feb 2026 20:32:11 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1771907531; x=1772512331; 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=Ilbe388Cxt5TpkwJ0RJZDtRFNc1WD3dk5cFWqwGlj9g=; b=V4tYjiiXIgillW77OZvVqxaJmARcT1j+Ccm/jdJ3eBITOGIXSl8fWkm0qPC9B5Pjkb i62l69WJw5OYE6ijS6I9+4/2zTkAh8PqGHhVSC3z8IhT9z3v2y2zk7oJddq6SEJ0uGaU UOVTJlPM/AtMZuPI9qIJm6il2DZGaS2MoaP38HR+Vd1UO5aZIdiMYq8CclEFVlxPaRGg MWWkG9nFlPmd9DNFfZ397ppPtd0FzYtBxtXPfBc95zshlC32yx8MNLYjvRCp1vCV+Ss+ c2k/rO8T8xe1h84htBcu5aJ/TLhqHFX32B9aHakfpUPNG/fKd4MfZJWIWM3JGop25nIQ hmqQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1771907531; x=1772512331; 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=Ilbe388Cxt5TpkwJ0RJZDtRFNc1WD3dk5cFWqwGlj9g=; b=D1dx66mwcCbahTz6HyOfmvlGsKGgTvggv+zKTm/aPE20Iq17mNovo35CGZU6Vkww+B 7qewgAdwkDee0PZHgl+I0lbsvd6rvwLaiq/Z6z0X5dXbUImtk9xqVLeNedAzPwF9EguT 2Gj8RrVddYcfhboLBnhr8f/pkpzirZTZ00yqRYDD6hs+Gst3uGQ2B9Oe/OR1orjPk4f0 fIHmyCBDi5BCKJSghc6ZvZZ/zrwly2jzlMn6a7Tkt0ue6ATEAcFUTSbJwwsjbokpMJU8 v5yeWttD+jceuoClmcYmk9tVtWA5/DFzIrpx04JNElhc1h4p1acOmSU9xS61U7hS4pYq FcyA== X-Forwarded-Encrypted: i=1; AJvYcCUfQMD/nkzB7Nk5ZBX+a4AmKPPhGbDYndGgo6HjDYZQKkPXOSXFnx8vSK+k1bt2GnUOof0DQMiBy/IWisg=@vger.kernel.org X-Gm-Message-State: AOJu0YwZlG3sThnUH+p7te6IkPmd66jay5LB67NsNKSSpaLPrYjPUfwb z9HFqWfVKdc8JoxawUuuh8GuDH2sWX9Uveu3mJ7DP7m0w5u0VSdID8+Z X-Gm-Gg: AZuq6aIOFOE6xsUF+xKNZVGIwfrjTham6nyJX+JrYaQYZWkREbWXfXj3yUzWYjuRbYY WZvV9boMJobqM/V2dElkoUcnbJgV3FNSm+f6IrgMHEL7VNz3H+h+/abMnWYk/AQAplaN/5O4L3T nTr3y5jmjHgDeEdDTMdARwCQql1aPN6iwQbth6aR83DOfWCvzjQJjehjBqzO27xcyHx8Qbh8tEa 29edTlY2hSdW4nXQ5OroRfn/dIm0fjIQ+ytSZJEFKFdfMMuimclqlDMDt++BDwheSr6IZkvGaOp 1kNFqWHUo2FZ4wG95U8qxPDERiX1fcZcQjVvcjEctDf/k6eeFRA9WpT8TF/sFYKZVeclCQYAimb bFmVrYlHE1rct0SO7nGMh48O8PnqyU/GVyWYOU0Nx+xswP5Q1hLO15tE2DW11EJ7/ygU2A3EJoJ X+GJARhTQIxV5uL3sooNc/Aph4z+noIoZkPef9HVNpLgI+xjXl84ABPI9Wktf9R3VtIQEes0wJI tU= X-Received: by 2002:a05:7022:685:b0:127:33e0:ea33 with SMTP id a92af1059eb24-1276ad11e4amr4942976c88.22.1771907530905; Mon, 23 Feb 2026 20:32:10 -0800 (PST) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-1276af7b4c7sm9607887c88.9.2026.02.23.20.32.10 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 23 Feb 2026 20:32:10 -0800 (PST) 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 v3 6/6] platform/x86: lenovo-wmi-other: Add WMI battery charge limiting. Date: Tue, 24 Feb 2026 04:32:00 +0000 Message-ID: <20260224043200.2680384-7-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20260224043200.2680384-1-derekjohn.clark@gmail.com> References: <20260224043200.2680384-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 function ID and capdata 00 ID 0x03010001 are able to enable or disable charging through the lenovo-wmi-other interface. The ideapad_laptop driver conflicts with this if it can also provide the attribute, so we have to get the acpi_handle and check for the same ACPI methods that enable the feature in that driver. The ACPI method is more reliable from my testing when both are present, so there is no need to modify the ideapad_laptop driver instead. Reviewed-by: Mark Pearson Signed-off-by: Derek J. Clark --- drivers/platform/x86/lenovo/wmi-capdata.h | 1 + drivers/platform/x86/lenovo/wmi-other.c | 230 ++++++++++++++++++++++ 2 files changed, 231 insertions(+) diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x= 86/lenovo/wmi-capdata.h index b7f9ee7b301a..00471551e7d6 100644 --- a/drivers/platform/x86/lenovo/wmi-capdata.h +++ b/drivers/platform/x86/lenovo/wmi-capdata.h @@ -26,6 +26,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 7f0d5a17b44f..b2daff1b45c2 100644 --- a/drivers/platform/x86/lenovo/wmi-other.c +++ b/drivers/platform/x86/lenovo/wmi-other.c @@ -42,9 +42,12 @@ #include #include #include +#include #include #include =20 +#include + #include "wmi-capdata.h" #include "wmi-events.h" #include "wmi-gamezone.h" @@ -78,10 +81,17 @@ enum lwmi_feature_id_gpu { LWMI_FEATURE_ID_GPU_NV_CPU_BOOST =3D 0x0b, }; =20 +enum lwmi_feature_id_psu { + LWMI_FEATURE_ID_PSU_INSTANT_MODE =3D 0x01, + LWMI_FEATURE_ID_PSU_CHARGE_MODE =3D 0x02, +}; + #define LWMI_FEATURE_ID_FAN_RPM 0x03 =20 #define LWMI_TYPE_ID_NONE 0x00 #define LWMI_TYPE_ID_CROSSLOAD 0x01 +#define LWMI_TYPE_ID_PSU_AC 0x01 +#define LWMI_TYPE_ID_PSU_PD 0x02 =20 #define LWMI_FEATURE_VALUE_GET 17 #define LWMI_FEATURE_VALUE_SET 18 @@ -92,10 +102,17 @@ enum lwmi_feature_id_gpu { =20 #define LWMI_FAN_DIV 100 =20 +#define LWMI_CHARGE_MODE_ENABLED 0x00 +#define LWMI_CHARGE_MODE_DISABLED 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 +154,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 +580,216 @@ 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_INSTANT_MODE, LWMI_TYP= E_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", retval= , prop); + + if (retval =3D=3D LWMI_CHARGE_MODE_DISABLED) + val->intval =3D POWER_SUPPLY_CHARGE_TYPE_LONGLIFE; + else + val->intval =3D 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 =3D ext_data; + struct wmi_method_args_32 args; + + args.arg0 =3D LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_INSTANT_MODE, LWMI_TYP= E_ID_PSU_AC); + if (val->intval =3D=3D POWER_SUPPLY_CHARGE_TYPE_LONGLIFE) + args.arg1 =3D LWMI_CHARGE_MODE_DISABLED; + else + args.arg1 =3D LWMI_CHARGE_MODE_ENABLED; + + 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 =3D ext_data; + struct capdata00 capdata; + u32 attribute_id =3D LWMI_ATTR_ID_PSU(LWMI_FEATURE_ID_PSU_INSTANT_MODE, L= WMI_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 (%#08x) 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) +{ + if (!handle) + return AE_OK; + + acpi_handle *ahand =3D context; + *ahand =3D handle; + + return AE_CTRL_TERMINATE; +} + +/** + * 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 */ + ret =3D acpi_get_devices(ideapad_hid, lwmi_acpi_match, &handle, NULL); + if (ret) + return; + + if (!handle) + return; + + if (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; + } + + /* 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 { @@ -1325,6 +1554,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.52.0