root/drivers/power/supply/macsmc-power.c
// SPDX-License-Identifier: GPL-2.0-only OR MIT
/*
 * Apple SMC Power/Battery Management Driver
 *
 * This driver exposes battery telemetry (voltage, current, temperature, health)
 * and AC adapter status provided by the Apple SMC (System Management Controller)
 * on Apple Silicon systems.
 *
 * Copyright The Asahi Linux Contributors
 */

#include <linux/ctype.h>
#include <linux/delay.h>
#include <linux/devm-helpers.h>
#include <linux/limits.h>
#include <linux/module.h>
#include <linux/mfd/macsmc.h>
#include <linux/notifier.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/reboot.h>
#include <linux/workqueue.h>

#define MAX_STRING_LENGTH 256

/*
 * The SMC reports charge in mAh (Coulombs) but energy in mWh (Joules).
 * We lack a register for "Nominal Voltage" or "Energy Accumulator".
 * We use a fixed 3.8V/cell constant to approximate energy stats for userspace,
 * derived from empirical data across supported MacBook models.
 */
#define MACSMC_NOMINAL_CELL_VOLTAGE_MV 3800

/* SMC Key Flags */
#define CHNC_BATTERY_FULL       BIT(0)
#define CHNC_NO_CHARGER         BIT(7)
#define CHNC_NOCHG_CH0C         BIT(14)
#define CHNC_NOCHG_CH0B_CH0K    BIT(15)
#define CHNC_BATTERY_FULL_2     BIT(18)
#define CHNC_BMS_BUSY           BIT(23)
#define CHNC_CHLS_LIMIT         BIT(24)
#define CHNC_NOAC_CH0J          BIT(53)
#define CHNC_NOAC_CH0I          BIT(54)

#define CH0R_LOWER_FLAGS        GENMASK(15, 0)
#define CH0R_NOAC_CH0I          BIT(0)
#define CH0R_NOAC_DISCONNECTED  BIT(4)
#define CH0R_NOAC_CH0J          BIT(5)
#define CH0R_BMS_BUSY           BIT(8)
#define CH0R_NOAC_CH0K          BIT(9)
#define CH0R_NOAC_CHWA          BIT(11)

#define CH0X_CH0C               BIT(0)
#define CH0X_CH0B               BIT(1)

#define ACSt_CAN_BOOT_AP        BIT(2)
#define ACSt_CAN_BOOT_IBOOT     BIT(1)

#define CHWA_CHLS_FIXED_START_OFFSET    5
#define CHLS_MIN_END_THRESHOLD          10
#define CHLS_FORCE_DISCHARGE            0x100
#define CHWA_FIXED_END_THRESHOLD        80
#define CHWA_PROP_WRITE_THRESHOLD       95

#define MACSMC_MAX_BATT_PROPS           50
#define MACSMC_MAX_AC_PROPS             10

struct macsmc_power {
        struct device *dev;
        struct apple_smc *smc;

        struct power_supply_desc ac_desc;
        struct power_supply_desc batt_desc;

        struct power_supply *batt;
        struct power_supply *ac;

        char model_name[MAX_STRING_LENGTH];
        char serial_number[MAX_STRING_LENGTH];
        char mfg_date[MAX_STRING_LENGTH];

        /* Supported feature flags based on SMC key presence */
        bool has_chwa; /* Charge limit (Modern firmware) */
        bool has_chls; /* Charge limit (Older firmware) */
        bool has_ch0i; /* Force discharge (Older firmware) */
        bool has_ch0c; /* Inhibit charge (Older firmware) */
        bool has_chte; /* Inhibit charge (Modern firmware) */
        /*
         * Battery critical key is 1 byte and charge key is little endian
         * (Modern firmware)
         */
        bool fw_ge_27;

        u8 num_cells;
        int nominal_voltage_mv;

        struct notifier_block nb;
        struct work_struct critical_work;
        bool emergency_shutdown_triggered;
        bool orderly_shutdown_triggered;
};

static int macsmc_battery_get_status(struct macsmc_power *power)
{
        u64 nocharge_flags;
        u32 nopower_flags;
        u16 ac_current;
        int charge_limit = 0;
        bool limited = false;
        bool flag;
        int ret;

        /*
         * B0AV (Voltage) is fundamental. If we can't read it, we assume the
         * battery is gone. CHCE (Hardware charger present) / CHCC (Hardware
         * charger capable) are fundamental status flags.
         * BSFC (System full charge) / CHSC (System charging) are fundamental
         * status flags.
         */

        /* Check if power input is inhibited (e.g. BMS balancing cycle) */
        ret = apple_smc_read_u32(power->smc, SMC_KEY(CH0R), &nopower_flags);
        if (!ret && (nopower_flags & CH0R_LOWER_FLAGS & ~CH0R_BMS_BUSY))
                return POWER_SUPPLY_STATUS_DISCHARGING;

        /* Check if charger is present */
        ret = apple_smc_read_flag(power->smc, SMC_KEY(CHCE), &flag);
        if (ret < 0)
                return ret;
        if (!flag)
                return POWER_SUPPLY_STATUS_DISCHARGING;

        /* Check if AC is charge capable */
        ret = apple_smc_read_flag(power->smc, SMC_KEY(CHCC), &flag);
        if (ret < 0)
                return ret;
        if (!flag)
                return POWER_SUPPLY_STATUS_DISCHARGING;

        /* Check if AC input limit is too low */
        ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-i), &ac_current);
        if (!ret && ac_current < 100)
                return POWER_SUPPLY_STATUS_DISCHARGING;

        /* Check if battery is full */
        ret = apple_smc_read_flag(power->smc, SMC_KEY(BSFC), &flag);
        if (ret < 0)
                return ret;
        if (flag)
                return POWER_SUPPLY_STATUS_FULL;

        /* Check for user-defined charge limits */
        if (power->has_chls) {
                u16 vu16;

                ret = apple_smc_read_u16(power->smc, SMC_KEY(CHLS), &vu16);
                if (ret == 0 && (vu16 & 0xff) >= CHLS_MIN_END_THRESHOLD)
                        charge_limit = (vu16 & 0xff) - CHWA_CHLS_FIXED_START_OFFSET;
        } else if (power->has_chwa) {
                ret = apple_smc_read_flag(power->smc, SMC_KEY(CHWA), &flag);
                if (ret == 0 && flag)
                        charge_limit = CHWA_FIXED_END_THRESHOLD - CHWA_CHLS_FIXED_START_OFFSET;
        }

        if (charge_limit > 0) {
                u8 buic = 0;

                if (apple_smc_read_u8(power->smc, SMC_KEY(BUIC), &buic) >= 0 &&
                    buic >= charge_limit)
                        limited = true;
        }

        /* Check charging inhibitors */
        ret = apple_smc_read_u64(power->smc, SMC_KEY(CHNC), &nocharge_flags);
        if (!ret) {
                if (nocharge_flags & CHNC_BATTERY_FULL)
                        return POWER_SUPPLY_STATUS_FULL;
                /* BMS busy shows up as inhibit, but we treat it as charging */
                else if (nocharge_flags == CHNC_BMS_BUSY && !limited)
                        return POWER_SUPPLY_STATUS_CHARGING;
                else if (nocharge_flags)
                        return POWER_SUPPLY_STATUS_NOT_CHARGING;
                else
                        return POWER_SUPPLY_STATUS_CHARGING;
        }

        /* Fallback: System charging flag */
        ret = apple_smc_read_flag(power->smc, SMC_KEY(CHSC), &flag);
        if (ret < 0)
                return ret;
        if (!flag)
                return POWER_SUPPLY_STATUS_NOT_CHARGING;

        return POWER_SUPPLY_STATUS_CHARGING;
}

static int macsmc_battery_get_charge_behaviour(struct macsmc_power *power)
{
        int ret;
        u8 val8;
        u8 chte_buf[4];

        if (power->has_ch0i) {
                ret = apple_smc_read_u8(power->smc, SMC_KEY(CH0I), &val8);
                if (ret)
                        return ret;
                if (val8 & CH0R_NOAC_CH0I)
                        return POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE;
        }

        if (power->has_chte) {
                ret = apple_smc_read(power->smc, SMC_KEY(CHTE), chte_buf, 4);
                if (ret < 0)
                        return ret;

                if (chte_buf[0] == 0x01)
                        return POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
        } else if (power->has_ch0c) {
                ret = apple_smc_read_u8(power->smc, SMC_KEY(CH0C), &val8);
                if (ret)
                        return ret;
                if (val8 & CH0X_CH0C)
                        return POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE;
        }

        return POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO;
}

static int macsmc_battery_set_charge_behaviour(struct macsmc_power *power, int val)
{
        int ret;

        switch (val) {
        case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO:
                /* Reset all inhibitors to a known-good 'auto' state */
                if (power->has_ch0i) {
                        ret = apple_smc_write_u8(power->smc, SMC_KEY(CH0I), 0);
                        if (ret)
                                return ret;
                }

                if (power->has_chte) {
                        ret = apple_smc_write_u32(power->smc, SMC_KEY(CHTE), 0);
                        if (ret)
                                return ret;
                } else if (power->has_ch0c) {
                        ret = apple_smc_write_u8(power->smc, SMC_KEY(CH0C), 0);
                        if (ret)
                                return ret;
                }
                return 0;

        case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE:
                if (power->has_chte)
                        return apple_smc_write_u32(power->smc, SMC_KEY(CHTE), 1);
                else if (power->has_ch0c)
                        return apple_smc_write_u8(power->smc, SMC_KEY(CH0C), 1);
                else
                        return -EOPNOTSUPP;

        case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE:
                if (!power->has_ch0i)
                        return -EOPNOTSUPP;
                return apple_smc_write_u8(power->smc, SMC_KEY(CH0I), 1);

        default:
                return -EINVAL;
        }
}

static int macsmc_battery_get_date(const char *s, int *out)
{
        if (!isdigit(s[0]) || !isdigit(s[1]))
                return -EOPNOTSUPP;

        *out = (s[0] - '0') * 10 + s[1] - '0';
        return 0;
}

static int macsmc_battery_read_bcf0(struct macsmc_power *power, u32 *val)
{
        u8 tval = 0;
        int ret;

        if (power->fw_ge_27) {
                ret = apple_smc_read_u8(power->smc, SMC_KEY(BCF0), &tval);
                *val = tval;
                return ret;
        }

        return apple_smc_read_u32(power->smc, SMC_KEY(BCF0), val);
}

static int macsmc_battery_get_capacity_level(struct macsmc_power *power)
{
        bool flag;
        u32 val;
        int ret;

        /* Check for emergency shutdown condition */
        if (macsmc_battery_read_bcf0(power, &val) >= 0 && val)
                return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;

        /* Check AC status for whether we could boot in this state */
        if (apple_smc_read_u32(power->smc, SMC_KEY(ACSt), &val) >= 0) {
                if (!(val & ACSt_CAN_BOOT_IBOOT))
                        return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;

                if (!(val & ACSt_CAN_BOOT_AP))
                        return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
        }

        /* BSFC = Battery System Full Charge */
        ret = apple_smc_read_flag(power->smc, SMC_KEY(BSFC), &flag);
        if (ret < 0)
                return POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN;

        if (flag)
                return POWER_SUPPLY_CAPACITY_LEVEL_FULL;
        else
                return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
}

static s16 macsmc_swap_b0rm(struct macsmc_power *power, s16 b0rm)
{
        /* B0RM was Big Endian, likely pass through from TI gas gauge */
        return power->fw_ge_27 ? b0rm : (s16)swab16(b0rm);
}

static int macsmc_battery_get_property(struct power_supply *psy,
                                       enum power_supply_property psp,
                                       union power_supply_propval *val)
{
        struct macsmc_power *power = power_supply_get_drvdata(psy);
        int ret = 0;
        u8 vu8;
        u16 vu16;
        s16 vs16;
        s32 vs32;
        s64 vs64;
        bool flag;

        switch (psp) {
        case POWER_SUPPLY_PROP_STATUS:
                val->intval = macsmc_battery_get_status(power);
                ret = val->intval < 0 ? val->intval : 0;
                break;
        case POWER_SUPPLY_PROP_PRESENT:
                val->intval = 1;
                break;
        case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
                val->intval = macsmc_battery_get_charge_behaviour(power);
                ret = val->intval < 0 ? val->intval : 0;
                break;
        case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0TE), &vu16);
                val->intval = vu16 == 0xffff ? 0 : vu16 * 60;
                break;
        case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0TF), &vu16);
                val->intval = vu16 == 0xffff ? 0 : vu16 * 60;
                break;
        case POWER_SUPPLY_PROP_CAPACITY:
                ret = apple_smc_read_u8(power->smc, SMC_KEY(BUIC), &vu8);
                val->intval = vu8;
                break;
        case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
                val->intval = macsmc_battery_get_capacity_level(power);
                ret = val->intval < 0 ? val->intval : 0;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CURRENT_NOW:
                ret = apple_smc_read_s16(power->smc, SMC_KEY(B0AC), &vs16);
                val->intval = vs16 * 1000;
                break;
        case POWER_SUPPLY_PROP_POWER_NOW:
                ret = apple_smc_read_s32(power->smc, SMC_KEY(B0AP), &vs32);
                val->intval = vs32 * 1000;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(BITV), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
                /* Calculate total max design voltage from per-cell maximum voltage */
                ret = apple_smc_read_u16(power->smc, SMC_KEY(BVVN), &vu16);
                val->intval = vu16 * 1000 * power->num_cells;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_MIN:
                /* Lifetime min */
                ret = apple_smc_read_s16(power->smc, SMC_KEY(BLPM), &vs16);
                val->intval = vs16 * 1000;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_MAX:
                /* Lifetime max */
                ret = apple_smc_read_s16(power->smc, SMC_KEY(BLPX), &vs16);
                val->intval = vs16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RC), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RI), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RV), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0DC), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CHARGE_FULL:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0FC), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_CHARGE_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RM), &vu16);
                val->intval = macsmc_swap_b0rm(power, vu16) * 1000;
                break;
        case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0DC), &vu16);
                val->intval = vu16 * power->nominal_voltage_mv;
                break;
        case POWER_SUPPLY_PROP_ENERGY_FULL:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0FC), &vu16);
                val->intval = vu16 * power->nominal_voltage_mv;
                break;
        case POWER_SUPPLY_PROP_ENERGY_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RM), &vu16);
                val->intval = macsmc_swap_b0rm(power, vu16) * power->nominal_voltage_mv;
                break;
        case POWER_SUPPLY_PROP_TEMP:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0AT), &vu16);
                val->intval = vu16 - 2732; /* Kelvin x10 to Celsius x10 */
                break;
        case POWER_SUPPLY_PROP_CHARGE_COUNTER:
                ret = apple_smc_read_s64(power->smc, SMC_KEY(BAAC), &vs64);
                val->intval = vs64;
                break;
        case POWER_SUPPLY_PROP_CYCLE_COUNT:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(B0CT), &vu16);
                val->intval = vu16;
                break;
        case POWER_SUPPLY_PROP_SCOPE:
                val->intval = POWER_SUPPLY_SCOPE_SYSTEM;
                break;
        case POWER_SUPPLY_PROP_HEALTH:
                flag = false;
                ret = apple_smc_read_flag(power->smc, SMC_KEY(BBAD), &flag);
                val->intval = flag ? POWER_SUPPLY_HEALTH_DEAD : POWER_SUPPLY_HEALTH_GOOD;
                break;
        case POWER_SUPPLY_PROP_MODEL_NAME:
                val->strval = power->model_name;
                break;
        case POWER_SUPPLY_PROP_SERIAL_NUMBER:
                val->strval = power->serial_number;
                break;
        case POWER_SUPPLY_PROP_MANUFACTURE_YEAR:
                ret = macsmc_battery_get_date(&power->mfg_date[0], &val->intval);
                /* The SMC reports the manufacture year as an offset from 1992. */
                val->intval += 1992;
                break;
        case POWER_SUPPLY_PROP_MANUFACTURE_MONTH:
                ret = macsmc_battery_get_date(&power->mfg_date[2], &val->intval);
                break;
        case POWER_SUPPLY_PROP_MANUFACTURE_DAY:
                ret = macsmc_battery_get_date(&power->mfg_date[4], &val->intval);
                break;
        default:
                return -EINVAL;
        }

        return ret;
}

static int macsmc_battery_set_property(struct power_supply *psy,
                                       enum power_supply_property psp,
                                       const union power_supply_propval *val)
{
        struct macsmc_power *power = power_supply_get_drvdata(psy);

        switch (psp) {
        case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
                return macsmc_battery_set_charge_behaviour(power, val->intval);
        default:
                return -EINVAL;
        }
}

static int macsmc_battery_property_is_writeable(struct power_supply *psy,
                                                enum power_supply_property psp)
{
        switch (psp) {
        case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR:
                return true;
        default:
                return false;
        }
}

static const struct power_supply_desc macsmc_battery_desc_template = {
        .name                   = "macsmc-battery",
        .type                   = POWER_SUPPLY_TYPE_BATTERY,
        .get_property           = macsmc_battery_get_property,
        .set_property           = macsmc_battery_set_property,
        .property_is_writeable  = macsmc_battery_property_is_writeable,
};

static int macsmc_ac_get_property(struct power_supply *psy,
                                  enum power_supply_property psp,
                                  union power_supply_propval *val)
{
        struct macsmc_power *power = power_supply_get_drvdata(psy);
        int ret = 0;
        u16 vu16;
        u32 vu32;

        switch (psp) {
        case POWER_SUPPLY_PROP_ONLINE:
                ret = apple_smc_read_u32(power->smc, SMC_KEY(CHIS), &vu32);
                val->intval = !!vu32;
                break;
        case POWER_SUPPLY_PROP_VOLTAGE_NOW:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-n), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT:
                ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-i), &vu16);
                val->intval = vu16 * 1000;
                break;
        case POWER_SUPPLY_PROP_INPUT_POWER_LIMIT:
                ret = apple_smc_read_u32(power->smc, SMC_KEY(ACPW), &vu32);
                val->intval = vu32 * 1000;
                break;
        default:
                return -EINVAL;
        }

        return ret;
}

static const struct power_supply_desc macsmc_ac_desc_template = {
        .name                   = "macsmc-ac",
        .type                   = POWER_SUPPLY_TYPE_MAINS,
        .get_property           = macsmc_ac_get_property,
};

static void macsmc_power_critical_work(struct work_struct *wrk)
{
        struct macsmc_power *power = container_of(wrk, struct macsmc_power, critical_work);
        u16 bitv, b0av;
        u32 bcf0;

        if (!power->batt)
                return;

        /*
         * Avoid duplicate atempts at emergency shutdown
         */
        if (power->emergency_shutdown_triggered || system_state > SYSTEM_RUNNING)
                return;

        /*
         * EMERGENCY: Check voltage vs design minimum.
         * If we are below BITV, the battery is physically exhausted.
         * We must shut down NOW to protect the filesystem.
         */
        if (apple_smc_read_u16(power->smc, SMC_KEY(BITV), &bitv) >= 0 &&
            apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &b0av) >= 0 &&
            b0av < bitv) {
                power->emergency_shutdown_triggered = true;
                dev_emerg(power->dev,
                          "Battery voltage (%d mV) below design minimum (%d mV)! Emergency shutdown.\n",
                          b0av, bitv);

                /*
                 * Shutdown is now imminent. Kick userspace again and give it some
                 * brief time to (hopefully) flush what's needed, before forcing.
                 */
                hw_protection_trigger("Battery voltage below design minimum", 1500);
        }

        /*
         * Avoid duplicate attempts at orderly shutdown.
         * Voltage check is above this as we may want to
         * "upgrade" an orderly shutdown to a critical power
         * off if voltage drops.
         */
        if (power->orderly_shutdown_triggered || system_state > SYSTEM_RUNNING)
                return;

        /*
         * Check if SMC flagged the battery as empty.
         * We trigger a graceful shutdown to let the OS save data.
         */
        if (macsmc_battery_read_bcf0(power, &bcf0) == 0 && bcf0 != 0) {
                power->orderly_shutdown_triggered = true;
                dev_crit(power->dev, "Battery critical (empty flag set). Triggering orderly shutdown.\n");
                orderly_poweroff(true);
        }
}

static int macsmc_power_event(struct notifier_block *nb, unsigned long event, void *data)
{
        struct macsmc_power *power = container_of(nb, struct macsmc_power, nb);

        /*
         * SMC Event IDs are correlated to physical events (e.g. charger
         * connect/disconnect) but the exact meaning of each ID is predicted.
         * 0x71... indicates power/battery events.
         */
        if ((event & 0xffffff00) == 0x71010100 || /* Charger status change */
            (event & 0xffff0000) == 0x71060000 || /* Port charge state change */
            (event & 0xffff0000) == 0x71130000) { /* Connector insert/remove event */
                if (power->batt)
                        power_supply_changed(power->batt);
                if (power->ac)
                        power_supply_changed(power->ac);
                return NOTIFY_OK;
        } else if (event == 0x71020000) {
                /* Critical battery warning */
                if (power->batt)
                        schedule_work(&power->critical_work);
                return NOTIFY_OK;
        }

        return NOTIFY_DONE;
}

static int macsmc_power_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct apple_smc *smc = dev_get_drvdata(pdev->dev.parent);
        struct power_supply_config psy_cfg = {};
        struct apple_smc_key_info info;
        struct macsmc_power *power;
        bool has_battery = false;
        bool has_ac_adapter = false;
        int ret = -ENODEV;
        bool flag;
        u16 vu16;
        u32 val32;
        enum power_supply_property *props;
        size_t nprops;

        if (!smc)
                return -ENODEV;

        power = devm_kzalloc(dev, sizeof(*power), GFP_KERNEL);
        if (!power)
                return -ENOMEM;

        power->dev = dev;
        power->smc = smc;
        dev_set_drvdata(dev, power);

        INIT_WORK(&power->critical_work, macsmc_power_critical_work);
        ret = devm_work_autocancel(dev, &power->critical_work, macsmc_power_critical_work);
        if (ret)
                return ret;

        /*
         * Check for battery presence.
         * B0AV is a fundamental key.
         */
        if (apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &vu16) == 0 &&
            macsmc_battery_get_status(power) > POWER_SUPPLY_STATUS_UNKNOWN)
                has_battery = true;

        /*
         * Check for AC adapter presence.
         * CHIS is a fundamental key.
         */
        if (apple_smc_key_exists(smc, SMC_KEY(CHIS)))
                has_ac_adapter = true;

        if (!has_battery && !has_ac_adapter)
                return -ENODEV;

        if (has_battery) {
                power->batt_desc = macsmc_battery_desc_template;
                props = devm_kcalloc(dev, MACSMC_MAX_BATT_PROPS,
                                     sizeof(enum power_supply_property),
                                     GFP_KERNEL);
                if (!props)
                        return -ENOMEM;

                nprops = 0;

                /* Fundamental properties */
                props[nprops++] = POWER_SUPPLY_PROP_STATUS;
                props[nprops++] = POWER_SUPPLY_PROP_PRESENT;
                props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_CURRENT_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_POWER_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_CAPACITY;
                props[nprops++] = POWER_SUPPLY_PROP_CAPACITY_LEVEL;
                props[nprops++] = POWER_SUPPLY_PROP_TEMP;
                props[nprops++] = POWER_SUPPLY_PROP_CYCLE_COUNT;
                props[nprops++] = POWER_SUPPLY_PROP_HEALTH;
                props[nprops++] = POWER_SUPPLY_PROP_SCOPE;
                props[nprops++] = POWER_SUPPLY_PROP_MODEL_NAME;
                props[nprops++] = POWER_SUPPLY_PROP_SERIAL_NUMBER;
                props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_YEAR;
                props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_MONTH;
                props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_DAY;

                /* Extended properties usually present */
                props[nprops++] = POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_TIME_TO_FULL_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN;
                props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN;
                props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MIN;
                props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MAX;
                props[nprops++] = POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT;
                props[nprops++] = POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX;
                props[nprops++] = POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE;
                props[nprops++] = POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN;
                props[nprops++] = POWER_SUPPLY_PROP_CHARGE_FULL;
                props[nprops++] = POWER_SUPPLY_PROP_CHARGE_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN;
                props[nprops++] = POWER_SUPPLY_PROP_ENERGY_FULL;
                props[nprops++] = POWER_SUPPLY_PROP_ENERGY_NOW;
                props[nprops++] = POWER_SUPPLY_PROP_CHARGE_COUNTER;

                /* Detect features based on key availability */
                if (apple_smc_key_exists(smc, SMC_KEY(CHTE)))
                        power->has_chte = true;
                if (apple_smc_key_exists(smc, SMC_KEY(CH0C)))
                        power->has_ch0c = true;
                if (apple_smc_key_exists(smc, SMC_KEY(CH0I)))
                        power->has_ch0i = true;

                ret = apple_smc_get_key_info(power->smc, SMC_KEY(BCF0), &info);
                if (ret) {
                        dev_err(&pdev->dev, "Failed to determine BCF0 key size\n");
                        return ret;
                }
                if (info.size == 1)
                        power->fw_ge_27 = true;
                else if (info.size == 4)
                        power->fw_ge_27 = false;
                else {
                        dev_err(&pdev->dev, "Unexpected BCF0 key size %d\n", info.size);
                        return -EIO;
                }

                /* Reset "Optimised Battery Charging" flags to default state */
                if (power->has_chte)
                        apple_smc_write_u32(smc, SMC_KEY(CHTE), 0);
                else if (power->has_ch0c)
                        apple_smc_write_u8(smc, SMC_KEY(CH0C), 0);

                if (power->has_ch0i)
                        apple_smc_write_u8(smc, SMC_KEY(CH0I), 0);

                apple_smc_write_u8(smc, SMC_KEY(CH0K), 0);
                apple_smc_write_u8(smc, SMC_KEY(CH0B), 0);

                /* Configure charge behaviour if supported */
                if (power->has_ch0i || power->has_ch0c || power->has_chte) {
                        props[nprops++] = POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR;

                        power->batt_desc.charge_behaviours =
                                BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO);

                        if (power->has_ch0i)
                                power->batt_desc.charge_behaviours |=
                                        BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE);

                        if (power->has_chte || power->has_ch0c)
                                power->batt_desc.charge_behaviours |=
                                        BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE);
                }

                /* Detect charge limit method (CHWA vs CHLS) */
                if (apple_smc_read_flag(power->smc, SMC_KEY(CHWA), &flag) == 0)
                        power->has_chwa = true;
                else if (apple_smc_read_u16(power->smc, SMC_KEY(CHLS), &vu16) >= 0)
                        power->has_chls = true;

                if (nprops > MACSMC_MAX_BATT_PROPS)
                        return -ENOMEM;

                power->batt_desc.properties = props;
                power->batt_desc.num_properties = nprops;

                /* Fetch identity strings */
                apple_smc_read(smc, SMC_KEY(BMDN), power->model_name,
                               sizeof(power->model_name) - 1);
                apple_smc_read(smc, SMC_KEY(BMSN), power->serial_number,
                               sizeof(power->serial_number) - 1);
                apple_smc_read(smc, SMC_KEY(BMDT), power->mfg_date,
                               sizeof(power->mfg_date) - 1);

                apple_smc_read_u8(power->smc, SMC_KEY(BNCB), &power->num_cells);
                power->nominal_voltage_mv = MACSMC_NOMINAL_CELL_VOLTAGE_MV * power->num_cells;

                /* Enable critical shutdown notifications by reading status once */
                macsmc_battery_read_bcf0(power, &val32);

                psy_cfg.drv_data = power;
                power->batt = devm_power_supply_register(dev, &power->batt_desc, &psy_cfg);
                if (IS_ERR(power->batt)) {
                        dev_err_probe(dev, PTR_ERR(power->batt),
                                      "Failed to register battery\n");
                        /* Don't return failure yet; try AC registration first */
                        power->batt = NULL;
                }
        }

        if (has_ac_adapter) {
                power->ac_desc = macsmc_ac_desc_template;
                props = devm_kcalloc(dev, MACSMC_MAX_AC_PROPS,
                                     sizeof(enum power_supply_property),
                                     GFP_KERNEL);
                if (!props)
                        return -ENOMEM;

                nprops = 0;

                /* Online status is fundamental */
                props[nprops++] = POWER_SUPPLY_PROP_ONLINE;

                /* Input power limits are usually available */
                if (apple_smc_key_exists(power->smc, SMC_KEY(ACPW)))
                        props[nprops++] = POWER_SUPPLY_PROP_INPUT_POWER_LIMIT;

                /* macOS 15.4+ firmware dropped legacy AC keys (AC-n, AC-i) */
                if (apple_smc_read_u16(power->smc, SMC_KEY(AC-n), &vu16) >= 0) {
                        props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_NOW;
                        props[nprops++] = POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT;
                }

                if (nprops > MACSMC_MAX_AC_PROPS)
                        return -ENOMEM;

                power->ac_desc.properties = props;
                power->ac_desc.num_properties = nprops;

                psy_cfg.drv_data = power;
                power->ac = devm_power_supply_register(dev, &power->ac_desc, &psy_cfg);
                if (IS_ERR(power->ac)) {
                        dev_err_probe(dev, PTR_ERR(power->ac),
                                      "Failed to register AC adapter\n");
                        power->ac = NULL;
                }
        }

        /* Final check: did we register anything? */
        if (!power->batt && !power->ac)
                return -ENODEV;

        power->nb.notifier_call = macsmc_power_event;
        blocking_notifier_chain_register(&smc->event_handlers, &power->nb);

        return 0;
}

static void macsmc_power_remove(struct platform_device *pdev)
{
        struct macsmc_power *power = dev_get_drvdata(&pdev->dev);

        blocking_notifier_chain_unregister(&power->smc->event_handlers, &power->nb);
}

static const struct platform_device_id macsmc_power_id[] = {
        { .name = "macsmc-power" },
        { /* sentinel */ }
};
MODULE_DEVICE_TABLE(platform, macsmc_power_id);

static struct platform_driver macsmc_power_driver = {
        .driver = {
                .name = "macsmc-power",
        },
        .id_table = macsmc_power_id,
        .probe = macsmc_power_probe,
        .remove = macsmc_power_remove,
};
module_platform_driver(macsmc_power_driver);

MODULE_LICENSE("Dual MIT/GPL");
MODULE_DESCRIPTION("Apple SMC battery and power management driver");
MODULE_AUTHOR("Hector Martin <marcan@marcan.st>");
MODULE_AUTHOR("Michael Reeves <michael.reeves077@gmail.com>");