root/drivers/platform/x86/dell/dell-wmi-ddv.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Linux driver for WMI sensor information on Dell notebooks.
 *
 * Copyright (C) 2022 Armin Wolf <W_Armin@gmx.de>
 */

#define pr_format(fmt) KBUILD_MODNAME ": " fmt

#include <linux/acpi.h>
#include <linux/bitfield.h>
#include <linux/debugfs.h>
#include <linux/device.h>
#include <linux/device/driver.h>
#include <linux/dev_printk.h>
#include <linux/errno.h>
#include <linux/kconfig.h>
#include <linux/kernel.h>
#include <linux/hwmon.h>
#include <linux/kstrtox.h>
#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/limits.h>
#include <linux/pm.h>
#include <linux/power_supply.h>
#include <linux/printk.h>
#include <linux/seq_file.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/wmi.h>

#include <acpi/battery.h>

#include <linux/unaligned.h>

#define DRIVER_NAME     "dell-wmi-ddv"

#define DELL_DDV_SUPPORTED_VERSION_MIN  2
#define DELL_DDV_SUPPORTED_VERSION_MAX  3
#define DELL_DDV_GUID   "8A42EA14-4F2A-FD45-6422-0087F7A7E608"

/* Battery indices 1, 2 and 3 */
#define DELL_DDV_NUM_BATTERIES          3

#define SBS_MANUFACTURE_YEAR_MASK       GENMASK(15, 9)
#define SBS_MANUFACTURE_MONTH_MASK      GENMASK(8, 5)
#define SBS_MANUFACTURE_DAY_MASK        GENMASK(4, 0)

#define MA_FAILURE_MODE_MASK                    GENMASK(11, 8)
#define MA_FAILURE_MODE_PERMANENT               0x9
#define MA_FAILURE_MODE_OVERHEAT                0xA
#define MA_FAILURE_MODE_OVERCURRENT             0xB

#define MA_PERMANENT_FAILURE_CODE_MASK          GENMASK(13, 12)
#define MA_PERMANENT_FAILURE_FUSE_BLOWN         0x0
#define MA_PERMANENT_FAILURE_CELL_IMBALANCE     0x1
#define MA_PERMANENT_FAILURE_OVERVOLTAGE        0x2
#define MA_PERMANENT_FAILURE_FET_FAILURE        0x3

#define MA_OVERHEAT_FAILURE_CODE_MASK           GENMASK(15, 12)
#define MA_OVERHEAT_FAILURE_START               0x5
#define MA_OVERHEAT_FAILURE_CHARGING            0x7
#define MA_OVERHEAT_FAILURE_DISCHARGING         0x8

#define MA_OVERCURRENT_FAILURE_CODE_MASK        GENMASK(15, 12)
#define MA_OVERCURRENT_FAILURE_CHARGING         0x6
#define MA_OVERCURRENT_FAILURE_DISCHARGING      0xB

#define DELL_EPPID_LENGTH       20
#define DELL_EPPID_EXT_LENGTH   23

static bool force;
module_param_unsafe(force, bool, 0);
MODULE_PARM_DESC(force, "Force loading without checking for supported WMI interface versions");

enum dell_ddv_method {
        DELL_DDV_BATTERY_DESIGN_CAPACITY        = 0x01,
        DELL_DDV_BATTERY_FULL_CHARGE_CAPACITY   = 0x02,
        DELL_DDV_BATTERY_MANUFACTURE_NAME       = 0x03,
        DELL_DDV_BATTERY_MANUFACTURE_DATE       = 0x04,
        DELL_DDV_BATTERY_SERIAL_NUMBER          = 0x05,
        DELL_DDV_BATTERY_CHEMISTRY_VALUE        = 0x06,
        DELL_DDV_BATTERY_TEMPERATURE            = 0x07,
        DELL_DDV_BATTERY_CURRENT                = 0x08,
        DELL_DDV_BATTERY_VOLTAGE                = 0x09,
        DELL_DDV_BATTERY_MANUFACTURER_ACCESS    = 0x0A,
        DELL_DDV_BATTERY_RELATIVE_CHARGE_STATE  = 0x0B,
        DELL_DDV_BATTERY_CYCLE_COUNT            = 0x0C,
        DELL_DDV_BATTERY_EPPID                  = 0x0D,
        DELL_DDV_BATTERY_RAW_ANALYTICS_START    = 0x0E,
        DELL_DDV_BATTERY_RAW_ANALYTICS          = 0x0F,
        DELL_DDV_BATTERY_DESIGN_VOLTAGE         = 0x10,
        DELL_DDV_BATTERY_RAW_ANALYTICS_A_BLOCK  = 0x11, /* version 3 */

        DELL_DDV_INTERFACE_VERSION              = 0x12,

        DELL_DDV_FAN_SENSOR_INFORMATION         = 0x20,
        DELL_DDV_THERMAL_SENSOR_INFORMATION     = 0x22,
};

struct fan_sensor_entry {
        u8 type;
        __le16 rpm;
} __packed;

struct thermal_sensor_entry {
        u8 type;
        s8 now;
        s8 min;
        s8 max;
        u8 unknown;
} __packed;

struct combined_channel_info {
        struct hwmon_channel_info info;
        u32 config[];
};

struct combined_chip_info {
        struct hwmon_chip_info chip;
        const struct hwmon_channel_info *info[];
};

struct dell_wmi_ddv_sensors {
        bool active;
        struct mutex lock;      /* protect caching */
        unsigned long timestamp;
        union acpi_object *obj;
        u64 entries;
};

struct dell_wmi_ddv_data {
        struct acpi_battery_hook hook;
        struct device_attribute eppid_attr;
        struct mutex translation_cache_lock;    /* Protects the translation cache */
        struct power_supply *translation_cache[DELL_DDV_NUM_BATTERIES];
        struct dell_wmi_ddv_sensors fans;
        struct dell_wmi_ddv_sensors temps;
        struct wmi_device *wdev;
};

static const char * const fan_labels[] = {
        "CPU Fan",
        "Chassis Motherboard Fan",
        "Video Fan",
        "Power Supply Fan",
        "Chipset Fan",
        "Memory Fan",
        "PCI Fan",
        "HDD Fan",
};

static const char * const fan_dock_labels[] = {
        "Docking Chassis/Motherboard Fan",
        "Docking Video Fan",
        "Docking Power Supply Fan",
        "Docking Chipset Fan",
};

static int dell_wmi_ddv_query_type(struct wmi_device *wdev, enum dell_ddv_method method, u32 arg,
                                   union acpi_object **result, acpi_object_type type)
{
        struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER, NULL };
        const struct acpi_buffer in = {
                .length = sizeof(arg),
                .pointer = &arg,
        };
        union acpi_object *obj;
        acpi_status ret;

        ret = wmidev_evaluate_method(wdev, 0x0, method, &in, &out);
        if (ACPI_FAILURE(ret))
                return -EIO;

        obj = out.pointer;
        if (!obj)
                return -ENODATA;

        if (obj->type != type) {
                kfree(obj);
                return -ENOMSG;
        }

        *result = obj;

        return 0;
}

static int dell_wmi_ddv_query_integer(struct wmi_device *wdev, enum dell_ddv_method method,
                                      u32 arg, u32 *res)
{
        union acpi_object *obj;
        int ret;

        ret = dell_wmi_ddv_query_type(wdev, method, arg, &obj, ACPI_TYPE_INTEGER);
        if (ret < 0)
                return ret;

        if (obj->integer.value <= U32_MAX)
                *res = (u32)obj->integer.value;
        else
                ret = -ERANGE;

        kfree(obj);

        return ret;
}

static int dell_wmi_ddv_query_buffer(struct wmi_device *wdev, enum dell_ddv_method method,
                                     u32 arg, union acpi_object **result)
{
        union acpi_object *obj;
        u64 buffer_size;
        int ret;

        ret = dell_wmi_ddv_query_type(wdev, method, arg, &obj, ACPI_TYPE_PACKAGE);
        if (ret < 0)
                return ret;

        if (obj->package.count != 2 ||
            obj->package.elements[0].type != ACPI_TYPE_INTEGER ||
            obj->package.elements[1].type != ACPI_TYPE_BUFFER) {
                ret = -ENOMSG;

                goto err_free;
        }

        buffer_size = obj->package.elements[0].integer.value;

        if (!buffer_size) {
                ret = -ENODATA;

                goto err_free;
        }

        if (buffer_size > obj->package.elements[1].buffer.length) {
                dev_warn(&wdev->dev,
                         FW_WARN "WMI buffer size (%llu) exceeds ACPI buffer size (%d)\n",
                         buffer_size, obj->package.elements[1].buffer.length);
                ret = -EMSGSIZE;

                goto err_free;
        }

        *result = obj;

        return 0;

err_free:
        kfree(obj);

        return ret;
}

static int dell_wmi_ddv_query_string(struct wmi_device *wdev, enum dell_ddv_method method,
                                     u32 arg, union acpi_object **result)
{
        return dell_wmi_ddv_query_type(wdev, method, arg, result, ACPI_TYPE_STRING);
}

/*
 * Needs to be called with lock held, except during initialization.
 */
static int dell_wmi_ddv_update_sensors(struct wmi_device *wdev, enum dell_ddv_method method,
                                       struct dell_wmi_ddv_sensors *sensors, size_t entry_size)
{
        u64 buffer_size, rem, entries;
        union acpi_object *obj;
        u8 *buffer;
        int ret;

        if (sensors->obj) {
                if (time_before(jiffies, sensors->timestamp + HZ))
                        return 0;

                kfree(sensors->obj);
                sensors->obj = NULL;
        }

        ret = dell_wmi_ddv_query_buffer(wdev, method, 0, &obj);
        if (ret < 0)
                return ret;

        /* buffer format sanity check */
        buffer_size = obj->package.elements[0].integer.value;
        buffer = obj->package.elements[1].buffer.pointer;
        entries = div64_u64_rem(buffer_size, entry_size, &rem);
        if (rem != 1 || buffer[buffer_size - 1] != 0xff) {
                ret = -ENOMSG;
                goto err_free;
        }

        if (!entries) {
                ret = -ENODATA;
                goto err_free;
        }

        sensors->obj = obj;
        sensors->entries = entries;
        sensors->timestamp = jiffies;

        return 0;

err_free:
        kfree(obj);

        return ret;
}

static umode_t dell_wmi_ddv_is_visible(const void *drvdata, enum hwmon_sensor_types type, u32 attr,
                                       int channel)
{
        return 0444;
}

static int dell_wmi_ddv_fan_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel,
                                         long *val)
{
        struct fan_sensor_entry *entry;
        int ret;

        ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION,
                                          &data->fans, sizeof(*entry));
        if (ret < 0)
                return ret;

        if (channel >= data->fans.entries)
                return -ENXIO;

        entry = (struct fan_sensor_entry *)data->fans.obj->package.elements[1].buffer.pointer;
        switch (attr) {
        case hwmon_fan_input:
                *val = get_unaligned_le16(&entry[channel].rpm);
                return 0;
        default:
                break;
        }

        return -EOPNOTSUPP;
}

static int dell_wmi_ddv_temp_read_channel(struct dell_wmi_ddv_data *data, u32 attr, int channel,
                                          long *val)
{
        struct thermal_sensor_entry *entry;
        int ret;

        ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION,
                                          &data->temps, sizeof(*entry));
        if (ret < 0)
                return ret;

        if (channel >= data->temps.entries)
                return -ENXIO;

        entry = (struct thermal_sensor_entry *)data->temps.obj->package.elements[1].buffer.pointer;
        switch (attr) {
        case hwmon_temp_input:
                *val = entry[channel].now * 1000;
                return 0;
        case hwmon_temp_min:
                *val = entry[channel].min * 1000;
                return 0;
        case hwmon_temp_max:
                *val = entry[channel].max * 1000;
                return 0;
        default:
                break;
        }

        return -EOPNOTSUPP;
}

static int dell_wmi_ddv_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
                             int channel, long *val)
{
        struct dell_wmi_ddv_data *data = dev_get_drvdata(dev);
        int ret;

        switch (type) {
        case hwmon_fan:
                mutex_lock(&data->fans.lock);
                ret = dell_wmi_ddv_fan_read_channel(data, attr, channel, val);
                mutex_unlock(&data->fans.lock);
                return ret;
        case hwmon_temp:
                mutex_lock(&data->temps.lock);
                ret = dell_wmi_ddv_temp_read_channel(data, attr, channel, val);
                mutex_unlock(&data->temps.lock);
                return ret;
        default:
                break;
        }

        return -EOPNOTSUPP;
}

static int dell_wmi_ddv_fan_read_string(struct dell_wmi_ddv_data *data, int channel,
                                        const char **str)
{
        struct fan_sensor_entry *entry;
        int ret;
        u8 type;

        ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_FAN_SENSOR_INFORMATION,
                                          &data->fans, sizeof(*entry));
        if (ret < 0)
                return ret;

        if (channel >= data->fans.entries)
                return -ENXIO;

        entry = (struct fan_sensor_entry *)data->fans.obj->package.elements[1].buffer.pointer;
        type = entry[channel].type;
        switch (type) {
        case 0x00 ... 0x07:
                *str = fan_labels[type];
                break;
        case 0x11 ... 0x14:
                *str = fan_dock_labels[type - 0x11];
                break;
        default:
                *str = "Unknown Fan";
                break;
        }

        return 0;
}

static int dell_wmi_ddv_temp_read_string(struct dell_wmi_ddv_data *data, int channel,
                                         const char **str)
{
        struct thermal_sensor_entry *entry;
        int ret;

        ret = dell_wmi_ddv_update_sensors(data->wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION,
                                          &data->temps, sizeof(*entry));
        if (ret < 0)
                return ret;

        if (channel >= data->temps.entries)
                return -ENXIO;

        entry = (struct thermal_sensor_entry *)data->temps.obj->package.elements[1].buffer.pointer;
        switch (entry[channel].type) {
        case 0x00:
                *str = "CPU";
                break;
        case 0x11:
                *str = "Video";
                break;
        case 0x22:
                *str = "Memory"; /* sometimes called DIMM */
                break;
        case 0x33:
                *str = "Other";
                break;
        case 0x44:
                *str = "Ambient"; /* sometimes called SKIN */
                break;
        case 0x52:
                *str = "SODIMM";
                break;
        case 0x55:
                *str = "HDD";
                break;
        case 0x62:
                *str = "SODIMM 2";
                break;
        case 0x73:
                *str = "NB";
                break;
        case 0x83:
                *str = "Charger";
                break;
        case 0xbb:
                *str = "Memory 3";
                break;
        default:
                *str = "Unknown";
                break;
        }

        return 0;
}

static int dell_wmi_ddv_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr,
                                    int channel, const char **str)
{
        struct dell_wmi_ddv_data *data = dev_get_drvdata(dev);
        int ret;

        switch (type) {
        case hwmon_fan:
                switch (attr) {
                case hwmon_fan_label:
                        mutex_lock(&data->fans.lock);
                        ret = dell_wmi_ddv_fan_read_string(data, channel, str);
                        mutex_unlock(&data->fans.lock);
                        return ret;
                default:
                        break;
                }
                break;
        case hwmon_temp:
                switch (attr) {
                case hwmon_temp_label:
                        mutex_lock(&data->temps.lock);
                        ret = dell_wmi_ddv_temp_read_string(data, channel, str);
                        mutex_unlock(&data->temps.lock);
                        return ret;
                default:
                        break;
                }
                break;
        default:
                break;
        }

        return -EOPNOTSUPP;
}

static const struct hwmon_ops dell_wmi_ddv_ops = {
        .is_visible = dell_wmi_ddv_is_visible,
        .read = dell_wmi_ddv_read,
        .read_string = dell_wmi_ddv_read_string,
};

static struct hwmon_channel_info *dell_wmi_ddv_channel_create(struct device *dev, u64 count,
                                                              enum hwmon_sensor_types type,
                                                              u32 config)
{
        struct combined_channel_info *cinfo;
        int i;

        cinfo = devm_kzalloc(dev, struct_size(cinfo, config, count + 1), GFP_KERNEL);
        if (!cinfo)
                return ERR_PTR(-ENOMEM);

        cinfo->info.type = type;
        cinfo->info.config = cinfo->config;

        for (i = 0; i < count; i++)
                cinfo->config[i] = config;

        return &cinfo->info;
}

static void dell_wmi_ddv_hwmon_cache_invalidate(struct dell_wmi_ddv_sensors *sensors)
{
        if (!sensors->active)
                return;

        mutex_lock(&sensors->lock);
        kfree(sensors->obj);
        sensors->obj = NULL;
        mutex_unlock(&sensors->lock);
}

static void dell_wmi_ddv_hwmon_cache_destroy(void *data)
{
        struct dell_wmi_ddv_sensors *sensors = data;

        sensors->active = false;
        mutex_destroy(&sensors->lock);
        kfree(sensors->obj);
}

static struct hwmon_channel_info *dell_wmi_ddv_channel_init(struct wmi_device *wdev,
                                                            enum dell_ddv_method method,
                                                            struct dell_wmi_ddv_sensors *sensors,
                                                            size_t entry_size,
                                                            enum hwmon_sensor_types type,
                                                            u32 config)
{
        struct hwmon_channel_info *info;
        int ret;

        ret = dell_wmi_ddv_update_sensors(wdev, method, sensors, entry_size);
        if (ret < 0)
                return ERR_PTR(ret);

        mutex_init(&sensors->lock);
        sensors->active = true;

        ret = devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors);
        if (ret < 0)
                return ERR_PTR(ret);

        info = dell_wmi_ddv_channel_create(&wdev->dev, sensors->entries, type, config);
        if (IS_ERR(info))
                devm_release_action(&wdev->dev, dell_wmi_ddv_hwmon_cache_destroy, sensors);

        return info;
}

static int dell_wmi_ddv_hwmon_add(struct dell_wmi_ddv_data *data)
{
        struct wmi_device *wdev = data->wdev;
        struct combined_chip_info *cinfo;
        struct hwmon_channel_info *info;
        struct device *hdev;
        int index = 0;
        int ret;

        if (!devres_open_group(&wdev->dev, dell_wmi_ddv_hwmon_add, GFP_KERNEL))
                return -ENOMEM;

        cinfo = devm_kzalloc(&wdev->dev, struct_size(cinfo, info, 4), GFP_KERNEL);
        if (!cinfo) {
                ret = -ENOMEM;

                goto err_release;
        }

        cinfo->chip.ops = &dell_wmi_ddv_ops;
        cinfo->chip.info = cinfo->info;

        info = dell_wmi_ddv_channel_create(&wdev->dev, 1, hwmon_chip, HWMON_C_REGISTER_TZ);
        if (IS_ERR(info)) {
                ret = PTR_ERR(info);

                goto err_release;
        }

        cinfo->info[index] = info;
        index++;

        info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_FAN_SENSOR_INFORMATION, &data->fans,
                                         sizeof(struct fan_sensor_entry), hwmon_fan,
                                         (HWMON_F_INPUT | HWMON_F_LABEL));
        if (!IS_ERR(info)) {
                cinfo->info[index] = info;
                index++;
        }

        info = dell_wmi_ddv_channel_init(wdev, DELL_DDV_THERMAL_SENSOR_INFORMATION, &data->temps,
                                         sizeof(struct thermal_sensor_entry), hwmon_temp,
                                         (HWMON_T_INPUT | HWMON_T_MIN | HWMON_T_MAX |
                                         HWMON_T_LABEL));
        if (!IS_ERR(info)) {
                cinfo->info[index] = info;
                index++;
        }

        if (index < 2) {
                /* Finding no available sensors is not an error */
                ret = 0;

                goto err_release;
        }

        hdev = devm_hwmon_device_register_with_info(&wdev->dev, "dell_ddv", data, &cinfo->chip,
                                                    NULL);
        if (IS_ERR(hdev)) {
                ret = PTR_ERR(hdev);

                goto err_release;
        }

        devres_close_group(&wdev->dev, dell_wmi_ddv_hwmon_add);

        return 0;

err_release:
        devres_release_group(&wdev->dev, dell_wmi_ddv_hwmon_add);

        return ret;
}

static int dell_wmi_ddv_battery_translate(struct dell_wmi_ddv_data *data,
                                          struct power_supply *battery, u32 *index)
{
        u32 serial_dec, serial_hex, serial;
        union power_supply_propval val;
        int ret;

        guard(mutex)(&data->translation_cache_lock);

        for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) {
                if (data->translation_cache[i] == battery) {
                        dev_dbg(&data->wdev->dev, "Translation cache hit for battery index %u\n",
                                i + 1);
                        *index = i + 1;
                        return 0;
                }
        }

        dev_dbg(&data->wdev->dev, "Translation cache miss\n");

        /*
         * Perform a translation between a ACPI battery and a battery index.
         * We have to use power_supply_get_property_direct() here because this
         * function will also get called from the callbacks of the power supply
         * extension.
         */
        ret = power_supply_get_property_direct(battery, POWER_SUPPLY_PROP_SERIAL_NUMBER, &val);
        if (ret < 0)
                return ret;

        /*
         * Some devices display the serial number of the ACPI battery (string!) as a decimal
         * number while other devices display it as a hexadecimal number. Because of this we
         * have to check both cases.
         */
        ret = kstrtou32(val.strval, 16, &serial_hex);
        if (ret < 0)
                return ret;     /* Should never fail */

        ret = kstrtou32(val.strval, 10, &serial_dec);
        if (ret < 0)
                serial_dec = 0; /* Can fail, thus we only mark serial_dec as invalid */

        for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) {
                ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_SERIAL_NUMBER, i + 1,
                                                 &serial);
                if (ret < 0)
                        return ret;

                /* A serial number of 0 signals that this index is not associated with a battery */
                if (!serial)
                        continue;

                if (serial == serial_dec || serial == serial_hex) {
                        dev_dbg(&data->wdev->dev, "Translation cache update for battery index %u\n",
                                i + 1);
                        data->translation_cache[i] = battery;
                        *index = i + 1;
                        return 0;
                }
        }

        return -ENODEV;
}

static void dell_wmi_battery_invalidate(struct dell_wmi_ddv_data *data,
                                        struct power_supply *battery)
{
        guard(mutex)(&data->translation_cache_lock);

        for (int i = 0; i < ARRAY_SIZE(data->translation_cache); i++) {
                if (data->translation_cache[i] == battery) {
                        data->translation_cache[i] = NULL;
                        return;
                }
        }
}

static ssize_t eppid_show(struct device *dev, struct device_attribute *attr, char *buf)
{
        struct dell_wmi_ddv_data *data = container_of(attr, struct dell_wmi_ddv_data, eppid_attr);
        union acpi_object *obj;
        u32 index;
        int ret;

        ret = dell_wmi_ddv_battery_translate(data, to_power_supply(dev), &index);
        if (ret < 0)
                return ret;

        ret = dell_wmi_ddv_query_string(data->wdev, DELL_DDV_BATTERY_EPPID, index, &obj);
        if (ret < 0)
                return ret;

        if (obj->string.length != DELL_EPPID_LENGTH && obj->string.length != DELL_EPPID_EXT_LENGTH)
                dev_info_once(&data->wdev->dev, FW_INFO "Suspicious ePPID length (%d)\n",
                              obj->string.length);

        ret = sysfs_emit(buf, "%s\n", obj->string.pointer);

        kfree(obj);

        return ret;
}

static int dell_wmi_ddv_get_health(struct dell_wmi_ddv_data *data, u32 index,
                                   union power_supply_propval *val)
{
        u32 value, code;
        int ret;

        ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_MANUFACTURER_ACCESS, index,
                                         &value);
        if (ret < 0)
                return ret;

        switch (FIELD_GET(MA_FAILURE_MODE_MASK, value)) {
        case MA_FAILURE_MODE_PERMANENT:
                code = FIELD_GET(MA_PERMANENT_FAILURE_CODE_MASK, value);
                switch (code) {
                case MA_PERMANENT_FAILURE_FUSE_BLOWN:
                        val->intval = POWER_SUPPLY_HEALTH_BLOWN_FUSE;
                        return 0;
                case MA_PERMANENT_FAILURE_CELL_IMBALANCE:
                        val->intval = POWER_SUPPLY_HEALTH_CELL_IMBALANCE;
                        return 0;
                case MA_PERMANENT_FAILURE_OVERVOLTAGE:
                        val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE;
                        return 0;
                case MA_PERMANENT_FAILURE_FET_FAILURE:
                        val->intval = POWER_SUPPLY_HEALTH_DEAD;
                        return 0;
                default:
                        dev_notice_once(&data->wdev->dev, "Unknown permanent failure code %u\n",
                                        code);
                        val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
                        return 0;
                }
        case MA_FAILURE_MODE_OVERHEAT:
                code = FIELD_GET(MA_OVERHEAT_FAILURE_CODE_MASK, value);
                switch (code) {
                case MA_OVERHEAT_FAILURE_START:
                case MA_OVERHEAT_FAILURE_CHARGING:
                case MA_OVERHEAT_FAILURE_DISCHARGING:
                        val->intval = POWER_SUPPLY_HEALTH_OVERHEAT;
                        return 0;
                default:
                        dev_notice_once(&data->wdev->dev, "Unknown overheat failure code %u\n",
                                        code);
                        val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
                        return 0;
                }
        case MA_FAILURE_MODE_OVERCURRENT:
                code = FIELD_GET(MA_OVERCURRENT_FAILURE_CODE_MASK, value);
                switch (code) {
                case MA_OVERCURRENT_FAILURE_CHARGING:
                case MA_OVERCURRENT_FAILURE_DISCHARGING:
                        val->intval = POWER_SUPPLY_HEALTH_OVERCURRENT;
                        return 0;
                default:
                        dev_notice_once(&data->wdev->dev, "Unknown overcurrent failure code %u\n",
                                        code);
                        val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
                        return 0;
                }
        default:
                val->intval = POWER_SUPPLY_HEALTH_GOOD;
                return 0;
        }
}

static int dell_wmi_ddv_get_manufacture_date(struct dell_wmi_ddv_data *data, u32 index,
                                             enum power_supply_property psp,
                                             union power_supply_propval *val)
{
        u16 year, month, day;
        u32 value;
        int ret;

        ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_MANUFACTURE_DATE,
                                         index, &value);
        if (ret < 0)
                return ret;
        if (value > U16_MAX)
                return -ENXIO;

        /*
         * Some devices report a invalid manufacture date value
         * like 0.0.1980. Because of this we have to check the
         * whole value before exposing parts of it to user space.
         */
        year = FIELD_GET(SBS_MANUFACTURE_YEAR_MASK, value) + 1980;
        month = FIELD_GET(SBS_MANUFACTURE_MONTH_MASK, value);
        if (month < 1 || month > 12)
                return -ENODATA;

        day = FIELD_GET(SBS_MANUFACTURE_DAY_MASK, value);
        if (day < 1 || day > 31)
                return -ENODATA;

        switch (psp) {
        case POWER_SUPPLY_PROP_MANUFACTURE_YEAR:
                val->intval = year;
                return 0;
        case POWER_SUPPLY_PROP_MANUFACTURE_MONTH:
                val->intval = month;
                return 0;
        case POWER_SUPPLY_PROP_MANUFACTURE_DAY:
                val->intval = day;
                return 0;
        default:
                return -EINVAL;
        }
}

static int dell_wmi_ddv_get_property(struct power_supply *psy, const struct power_supply_ext *ext,
                                     void *drvdata, enum power_supply_property psp,
                                     union power_supply_propval *val)
{
        struct dell_wmi_ddv_data *data = drvdata;
        u32 index, value;
        int ret;

        ret = dell_wmi_ddv_battery_translate(data, psy, &index);
        if (ret < 0)
                return ret;

        switch (psp) {
        case POWER_SUPPLY_PROP_HEALTH:
                return dell_wmi_ddv_get_health(data, index, val);
        case POWER_SUPPLY_PROP_TEMP:
                ret = dell_wmi_ddv_query_integer(data->wdev, DELL_DDV_BATTERY_TEMPERATURE, index,
                                                 &value);
                if (ret < 0)
                        return ret;

                /* Use 2732 instead of 2731.5 to avoid unnecessary rounding and to emulate
                 * the behaviour of the OEM application which seems to round down the result.
                 */
                val->intval = value - 2732;
                return 0;
        case POWER_SUPPLY_PROP_MANUFACTURE_YEAR:
        case POWER_SUPPLY_PROP_MANUFACTURE_MONTH:
        case POWER_SUPPLY_PROP_MANUFACTURE_DAY:
                return dell_wmi_ddv_get_manufacture_date(data, index, psp, val);
        default:
                return -EINVAL;
        }
}

static const enum power_supply_property dell_wmi_ddv_properties[] = {
        POWER_SUPPLY_PROP_HEALTH,
        POWER_SUPPLY_PROP_TEMP,
        POWER_SUPPLY_PROP_MANUFACTURE_YEAR,
        POWER_SUPPLY_PROP_MANUFACTURE_MONTH,
        POWER_SUPPLY_PROP_MANUFACTURE_DAY,
};

static const struct power_supply_ext dell_wmi_ddv_extension = {
        .name = DRIVER_NAME,
        .properties = dell_wmi_ddv_properties,
        .num_properties = ARRAY_SIZE(dell_wmi_ddv_properties),
        .get_property = dell_wmi_ddv_get_property,
};

static int dell_wmi_ddv_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
{
        struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook);
        int ret;

        /*
         * We cannot do the battery matching here since the battery might be absent, preventing
         * us from reading the serial number.
         */

        ret = device_create_file(&battery->dev, &data->eppid_attr);
        if (ret < 0)
                return ret;

        ret = power_supply_register_extension(battery, &dell_wmi_ddv_extension, &data->wdev->dev,
                                              data);
        if (ret < 0) {
                device_remove_file(&battery->dev, &data->eppid_attr);

                return ret;
        }

        return 0;
}

static int dell_wmi_ddv_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
{
        struct dell_wmi_ddv_data *data = container_of(hook, struct dell_wmi_ddv_data, hook);

        device_remove_file(&battery->dev, &data->eppid_attr);
        power_supply_unregister_extension(battery, &dell_wmi_ddv_extension);

        dell_wmi_battery_invalidate(data, battery);

        return 0;
}

static int dell_wmi_ddv_battery_add(struct dell_wmi_ddv_data *data)
{
        int ret;

        ret = devm_mutex_init(&data->wdev->dev, &data->translation_cache_lock);
        if (ret < 0)
                return ret;

        data->hook.name = "Dell DDV Battery Extension";
        data->hook.add_battery = dell_wmi_ddv_add_battery;
        data->hook.remove_battery = dell_wmi_ddv_remove_battery;

        sysfs_attr_init(&data->eppid_attr.attr);
        data->eppid_attr.attr.name = "eppid";
        data->eppid_attr.attr.mode = 0444;
        data->eppid_attr.show = eppid_show;

        return devm_battery_hook_register(&data->wdev->dev, &data->hook);
}

static int dell_wmi_ddv_buffer_read(struct seq_file *seq, enum dell_ddv_method method)
{
        struct device *dev = seq->private;
        struct dell_wmi_ddv_data *data = dev_get_drvdata(dev);
        union acpi_object *obj;
        u64 size;
        u8 *buf;
        int ret;

        ret = dell_wmi_ddv_query_buffer(data->wdev, method, 0, &obj);
        if (ret < 0)
                return ret;

        size = obj->package.elements[0].integer.value;
        buf = obj->package.elements[1].buffer.pointer;
        ret = seq_write(seq, buf, size);
        kfree(obj);

        return ret;
}

static int dell_wmi_ddv_fan_read(struct seq_file *seq, void *offset)
{
        return dell_wmi_ddv_buffer_read(seq, DELL_DDV_FAN_SENSOR_INFORMATION);
}

static int dell_wmi_ddv_temp_read(struct seq_file *seq, void *offset)
{
        return dell_wmi_ddv_buffer_read(seq, DELL_DDV_THERMAL_SENSOR_INFORMATION);
}

static void dell_wmi_ddv_debugfs_remove(void *data)
{
        struct dentry *entry = data;

        debugfs_remove(entry);
}

static void dell_wmi_ddv_debugfs_init(struct wmi_device *wdev)
{
        struct dentry *entry;
        char name[64];

        scnprintf(name, ARRAY_SIZE(name), "%s-%s", DRIVER_NAME, dev_name(&wdev->dev));
        entry = debugfs_create_dir(name, NULL);

        debugfs_create_devm_seqfile(&wdev->dev, "fan_sensor_information", entry,
                                    dell_wmi_ddv_fan_read);
        debugfs_create_devm_seqfile(&wdev->dev, "thermal_sensor_information", entry,
                                    dell_wmi_ddv_temp_read);

        devm_add_action_or_reset(&wdev->dev, dell_wmi_ddv_debugfs_remove, entry);
}

static int dell_wmi_ddv_probe(struct wmi_device *wdev, const void *context)
{
        struct dell_wmi_ddv_data *data;
        u32 version;
        int ret;

        ret = dell_wmi_ddv_query_integer(wdev, DELL_DDV_INTERFACE_VERSION, 0, &version);
        if (ret < 0)
                return ret;

        dev_dbg(&wdev->dev, "WMI interface version: %d\n", version);
        if (version < DELL_DDV_SUPPORTED_VERSION_MIN || version > DELL_DDV_SUPPORTED_VERSION_MAX) {
                if (!force)
                        return -ENODEV;

                dev_warn(&wdev->dev, "Loading despite unsupported WMI interface version (%u)\n",
                         version);
        }

        data = devm_kzalloc(&wdev->dev, sizeof(*data), GFP_KERNEL);
        if (!data)
                return -ENOMEM;

        dev_set_drvdata(&wdev->dev, data);
        data->wdev = wdev;

        dell_wmi_ddv_debugfs_init(wdev);

        if (IS_REACHABLE(CONFIG_ACPI_BATTERY)) {
                ret = dell_wmi_ddv_battery_add(data);
                if (ret < 0)
                        dev_warn(&wdev->dev, "Unable to register ACPI battery hook: %d\n", ret);
        }

        if (IS_REACHABLE(CONFIG_HWMON)) {
                ret = dell_wmi_ddv_hwmon_add(data);
                if (ret < 0)
                        dev_warn(&wdev->dev, "Unable to register hwmon interface: %d\n", ret);
        }

        return 0;
}

static int dell_wmi_ddv_resume(struct device *dev)
{
        struct dell_wmi_ddv_data *data = dev_get_drvdata(dev);

        /* Force re-reading of all active sensors */
        dell_wmi_ddv_hwmon_cache_invalidate(&data->fans);
        dell_wmi_ddv_hwmon_cache_invalidate(&data->temps);

        return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(dell_wmi_ddv_dev_pm_ops, NULL, dell_wmi_ddv_resume);

static const struct wmi_device_id dell_wmi_ddv_id_table[] = {
        { DELL_DDV_GUID, NULL },
        { }
};
MODULE_DEVICE_TABLE(wmi, dell_wmi_ddv_id_table);

static struct wmi_driver dell_wmi_ddv_driver = {
        .driver = {
                .name = DRIVER_NAME,
                .probe_type = PROBE_PREFER_ASYNCHRONOUS,
                .pm = pm_sleep_ptr(&dell_wmi_ddv_dev_pm_ops),
        },
        .id_table = dell_wmi_ddv_id_table,
        .probe = dell_wmi_ddv_probe,
        .no_singleton = true,
};
module_wmi_driver(dell_wmi_ddv_driver);

MODULE_AUTHOR("Armin Wolf <W_Armin@gmx.de>");
MODULE_DESCRIPTION("Dell WMI sensor driver");
MODULE_LICENSE("GPL");