platform/x86: system76_acpi: Add battery charging thresholds
authorTim Crawford <tcrawford@system76.com>
Wed, 6 Oct 2021 20:22:01 +0000 (14:22 -0600)
committerHans de Goede <hdegoede@redhat.com>
Tue, 19 Oct 2021 14:31:09 +0000 (16:31 +0200)
System76 laptops running open source EC firmware support configuring
charging thresholds through ACPI methods. Expose this functionality
through the standard sysfs entries charge_control_{start,end}_threshold.

Signed-off-by: Tim Crawford <tcrawford@system76.com>
Link: https://lore.kernel.org/r/20211006202202.7479-4-tcrawford@system76.com
Reviewed-by: Hans de Goede <hdegoede@redhat.com>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
drivers/platform/x86/system76_acpi.c

index 9e525b5..70d0490 100644 (file)
 #include <linux/leds.h>
 #include <linux/module.h>
 #include <linux/pci_ids.h>
+#include <linux/power_supply.h>
+#include <linux/sysfs.h>
 #include <linux/types.h>
 
+#include <acpi/battery.h>
+
 struct system76_data {
        struct acpi_device *acpi_dev;
        struct led_classdev ap_led;
@@ -143,6 +147,154 @@ static int system76_set(struct system76_data *data, char *method, int value)
                return -1;
 }
 
+#define BATTERY_THRESHOLD_INVALID      0xFF
+
+enum {
+       THRESHOLD_START,
+       THRESHOLD_END,
+};
+
+static ssize_t battery_get_threshold(int which, char *buf)
+{
+       struct acpi_object_list input;
+       union acpi_object param;
+       acpi_handle handle;
+       acpi_status status;
+       unsigned long long ret = BATTERY_THRESHOLD_INVALID;
+
+       handle = ec_get_handle();
+       if (!handle)
+               return -ENODEV;
+
+       input.count = 1;
+       input.pointer = &param;
+       // Start/stop selection
+       param.type = ACPI_TYPE_INTEGER;
+       param.integer.value = which;
+
+       status = acpi_evaluate_integer(handle, "GBCT", &input, &ret);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+       if (ret == BATTERY_THRESHOLD_INVALID)
+               return -EINVAL;
+
+       return sysfs_emit(buf, "%d\n", (int)ret);
+}
+
+static ssize_t battery_set_threshold(int which, const char *buf, size_t count)
+{
+       struct acpi_object_list input;
+       union acpi_object params[2];
+       acpi_handle handle;
+       acpi_status status;
+       unsigned int value;
+       int ret;
+
+       handle = ec_get_handle();
+       if (!handle)
+               return -ENODEV;
+
+       ret = kstrtouint(buf, 10, &value);
+       if (ret)
+               return ret;
+
+       if (value > 100)
+               return -EINVAL;
+
+       input.count = 2;
+       input.pointer = params;
+       // Start/stop selection
+       params[0].type = ACPI_TYPE_INTEGER;
+       params[0].integer.value = which;
+       // Threshold value
+       params[1].type = ACPI_TYPE_INTEGER;
+       params[1].integer.value = value;
+
+       status = acpi_evaluate_object(handle, "SBCT", &input, NULL);
+       if (ACPI_FAILURE(status))
+               return -EIO;
+
+       return count;
+}
+
+static ssize_t charge_control_start_threshold_show(struct device *dev,
+       struct device_attribute *attr, char *buf)
+{
+       return battery_get_threshold(THRESHOLD_START, buf);
+}
+
+static ssize_t charge_control_start_threshold_store(struct device *dev,
+       struct device_attribute *attr, const char *buf, size_t count)
+{
+       return battery_set_threshold(THRESHOLD_START, buf, count);
+}
+
+static DEVICE_ATTR_RW(charge_control_start_threshold);
+
+static ssize_t charge_control_end_threshold_show(struct device *dev,
+       struct device_attribute *attr, char *buf)
+{
+       return battery_get_threshold(THRESHOLD_END, buf);
+}
+
+static ssize_t charge_control_end_threshold_store(struct device *dev,
+       struct device_attribute *attr, const char *buf, size_t count)
+{
+       return battery_set_threshold(THRESHOLD_END, buf, count);
+}
+
+static DEVICE_ATTR_RW(charge_control_end_threshold);
+
+static struct attribute *system76_battery_attrs[] = {
+       &dev_attr_charge_control_start_threshold.attr,
+       &dev_attr_charge_control_end_threshold.attr,
+       NULL,
+};
+
+ATTRIBUTE_GROUPS(system76_battery);
+
+static int system76_battery_add(struct power_supply *battery)
+{
+       // System76 EC only supports 1 battery
+       if (strcmp(battery->desc->name, "BAT0") != 0)
+               return -ENODEV;
+
+       if (device_add_groups(&battery->dev, system76_battery_groups))
+               return -ENODEV;
+
+       return 0;
+}
+
+static int system76_battery_remove(struct power_supply *battery)
+{
+       device_remove_groups(&battery->dev, system76_battery_groups);
+       return 0;
+}
+
+static struct acpi_battery_hook system76_battery_hook = {
+       .add_battery = system76_battery_add,
+       .remove_battery = system76_battery_remove,
+       .name = "System76 Battery Extension",
+};
+
+static void system76_battery_init(void)
+{
+       acpi_handle handle;
+
+       handle = ec_get_handle();
+       if (handle && acpi_has_method(handle, "GBCT"))
+               battery_hook_register(&system76_battery_hook);
+}
+
+static void system76_battery_exit(void)
+{
+       acpi_handle handle;
+
+       handle = ec_get_handle();
+       if (handle && acpi_has_method(handle, "GBCT"))
+               battery_hook_unregister(&system76_battery_hook);
+}
+
 // Get the airplane mode LED brightness
 static enum led_brightness ap_led_get(struct led_classdev *led)
 {
@@ -581,6 +733,8 @@ static int system76_add(struct acpi_device *acpi_dev)
        if (err)
                goto error;
 
+       system76_battery_init();
+
        return 0;
 
 error:
@@ -596,6 +750,9 @@ static int system76_remove(struct acpi_device *acpi_dev)
        struct system76_data *data;
 
        data = acpi_driver_data(acpi_dev);
+
+       system76_battery_exit();
+
        if (data->kb_color >= 0)
                device_remove_file(data->kb_led.dev, &kb_led_color_dev_attr);