up-device-supply: read charge_type from the supplying charger

On a discrete-PMIC platform the fuel gauge and the charger are separate
devices and only the charger knows the charge regime. On SDM845
qcom-battery is fuel-gauge@4000 and exposes no charge_type at all, while
pmi8998-charger is charger@1000 and does, so reading only the battery's
own attribute returned UNKNOWN on every refresh.

Follow the device links the kernel already publishes rather than guessing
which sibling supply is the charger:

  <native>/device/supplier:* -> /sys/devices/virtual/devlink/<s>--<c>
  <devlink>/supplier         -> the supplying device
  <supplier>/power_supply/<name>/charge_type

The battery's own attribute still wins when present, so this is inert on
hardware where the gauge reports charge_type itself.
This commit is contained in:
Fimeg 2026-07-25 06:43:56 -04:00
commit 718e9210d7

View file

@ -198,12 +198,109 @@ up_device_supply_get_state (GUdevDevice *native)
return state;
}
/**
* up_device_supply_get_supplier_charge_type_str:
*
* Reads `charge_type` from the power supply that *supplies* @native.
*
* On a discrete-PMIC platform the fuel gauge and the charger are separate
* devices, and only the charger knows the charge regime. Qualcomm SDM845 is
* the case in hand: `qcom-battery` is `fuel-gauge@4000` and exposes no
* `charge_type` at all, while `pmi8998-charger` is `charger@1000` and does.
* Reading only @native's own attribute yields UNKNOWN forever on such a
* device.
*
* The kernel already publishes the relation as device links, so follow it
* instead of guessing which sibling supply is the charger:
*
* <native>/device/supplier:<...> -> /sys/devices/virtual/devlink/<s>--<c>
* <devlink>/supplier -> the supplying device
* <supplier>/power_supply/<name>/charge_type
*
* Returns the raw attribute string (caller frees), or %NULL when there is no
* supplier exposing one.
**/
static gchar *
up_device_supply_get_supplier_charge_type_str (GUdevDevice *native)
{
g_autoptr(GUdevDevice) parent = NULL;
g_autoptr(GDir) dir = NULL;
const gchar *parent_path;
const gchar *entry;
parent = g_udev_device_get_parent (native);
if (parent == NULL)
return NULL;
parent_path = g_udev_device_get_sysfs_path (parent);
if (parent_path == NULL)
return NULL;
dir = g_dir_open (parent_path, 0, NULL);
if (dir == NULL)
return NULL;
while ((entry = g_dir_read_name (dir)) != NULL) {
g_autofree gchar *link_path = NULL;
g_autofree gchar *link_target = NULL;
g_autofree gchar *devlink_path = NULL;
g_autofree gchar *supplier_link = NULL;
g_autofree gchar *supplier_target = NULL;
g_autofree gchar *supplier_path = NULL;
g_autofree gchar *ps_path = NULL;
g_autoptr(GDir) ps_dir = NULL;
const gchar *ps_entry;
if (!g_str_has_prefix (entry, "supplier:"))
continue;
/* <native>/device/supplier:* -> the devlink node */
link_path = g_build_filename (parent_path, entry, NULL);
link_target = g_file_read_link (link_path, NULL);
if (link_target == NULL)
continue;
devlink_path = g_canonicalize_filename (link_target, parent_path);
/* <devlink>/supplier -> the supplying device */
supplier_link = g_build_filename (devlink_path, "supplier", NULL);
supplier_target = g_file_read_link (supplier_link, NULL);
if (supplier_target == NULL)
continue;
supplier_path = g_canonicalize_filename (supplier_target, devlink_path);
/* Not every supplier is a power supply — the SPMI bus controller
* is a supplier here too, and has no power_supply child. */
ps_path = g_build_filename (supplier_path, "power_supply", NULL);
ps_dir = g_dir_open (ps_path, 0, NULL);
if (ps_dir == NULL)
continue;
while ((ps_entry = g_dir_read_name (ps_dir)) != NULL) {
g_autofree gchar *attr_path = NULL;
gchar *contents = NULL;
attr_path = g_build_filename (ps_path, ps_entry, "charge_type", NULL);
if (!g_file_get_contents (attr_path, &contents, NULL, NULL))
continue;
g_strstrip (contents);
if (contents[0] != '\0')
return contents;
g_free (contents);
}
}
return NULL;
}
/**
* up_device_supply_get_charge_type:
*
* Reads the kernel `charge_type` sysfs attribute and maps it to a
* #UpDeviceChargeType. Returns %UP_DEVICE_CHARGE_TYPE_UNKNOWN if the
* driver doesn't expose the attribute, and
* #UpDeviceChargeType. When @native carries no such attribute the value is
* resolved from the supplying charger instead (see
* up_device_supply_get_supplier_charge_type_str()). Returns
* %UP_DEVICE_CHARGE_TYPE_UNKNOWN only when neither exposes it, and
* %UP_DEVICE_CHARGE_TYPE_NONE for an empty/unset value (no charger
* active).
**/
@ -214,6 +311,8 @@ up_device_supply_get_charge_type (GUdevDevice *native)
UpDeviceChargeType charge_type;
charge_type_str = up_device_supply_get_string (native, "charge_type");
if (charge_type_str == NULL)
charge_type_str = up_device_supply_get_supplier_charge_type_str (native);
if (charge_type_str == NULL)
return UP_DEVICE_CHARGE_TYPE_UNKNOWN;