// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  legion-laptop.c - Extra Lenovo Legion laptop support, in
 *   particular for fan curve control and power mode.
 *
 *  Copyright (C) 2022 johnfan <johnfan (at) example (dot) com>
 *
 *
 *  This driver might work on other Lenovo Legion models. If you
 *  want to try it you can pass force=1 as argument
 *  to the module which will force it to load even when the DMI
 *  data doesn't match the model AND FIRMWARE.
 *
 *  Support for other hardware of this model is already partially
 *  provided by the module ideapad-laptop.
 *
 *  The development page for this driver is located at
 *  https://github.com/johnfanv2/LenovoLegionLinux
 *
 *  This driver exports the files:
 *    - /sys/kernel/debug/legion/fancurve (ro)
 *        The fan curve stored in the firmware in the form of a
 *        human readable table.
 *
 *    - /sys/module/legion_laptop/drivers/platform\:legion/PNP0C09\:00/powermode (rw)
 *       0: balanced mode (white)
 *       1: performance mode (red)
 *       2: quiet mode (blue)
 *       ?: custom mode (pink)
 *
 *  NOTE: Writing to this will load the default fan curve from
 *        the firmware for this mode, so the fan curve might
 *        have to be reconfigured if needed.
 *
 *  It implements the usual hwmon interface to monitor fan speed and temmperature
 *  and allows to set the fan curve inside the firmware.
 *
 *    - /sys/class/hwmon/X/fan1_input or /sys/class/hwmon/X/fan2_input  (ro)
 *        Current fan speed of fan1/fan2.
 *    - /sys/class/hwmon/X/temp1_input (ro)
 *    - /sys/class/hwmon/X/temp2_input (ro)
 *    - /sys/class/hwmon/X/temp3_input (ro)
 *        Temperature (Celsius) of CPU, GPU, and IC used for fan control.
 *    - /sys/class/hwmon/X/pwmY_auto_pointZ_pwm (rw)
 *          PWM (0-255) of the fan at the Y-level in the fan curve
 *    - /sys/class/hwmon/X/pwmY_auto_pointZ_temp (rw)
 *          upper temperature of tempZ (CPU, GPU, or IC) at the Y-level in the fan curve
 *    - /sys/class/hwmon/X/pwmY_auto_pointZ_temp_hyst (rw)
 *          hysteris (CPU, GPU, or IC) at the Y-level in the fan curve. The lower
 *          temperatue of the level is the upper temperature minus the hysteris
 *
 *
 *  Credits for reverse engineering the firmware to:
 *      - David Woodhouse: heavily inspired by lenovo_laptop.c
 *      - Luke Cama: Windows version "LegionFanControl"
 *      - SmokelessCPU: reverse engineering of custom registers in EC
 *                      and commincation method with EC via ports
 *      - 0x1F9F1: additional reverse engineering for complete fan curve
 */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/acpi.h>
#include <asm/io.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/dmi.h>
#include <linux/efi.h>
#include <linux/leds.h>
#include <linux/hwmon.h>
#include <linux/hwmon-sysfs.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/platform_profile.h>
#include <linux/types.h>
#include <linux/wmi.h>
#include <linux/version.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("johnfan");
MODULE_DESCRIPTION("Lenovo Legion laptop extras");

static bool force;
module_param(force, bool, 0440);
MODULE_PARM_DESC(
	force,
	"Force loading this module even if model or BIOS does not match.");

static bool ec_readonly;
module_param(ec_readonly, bool, 0440);
MODULE_PARM_DESC(
	ec_readonly,
	"Only read from embedded controller but do not write or change settings.");

static bool enable_platformprofile = true;
module_param(enable_platformprofile, bool, 0440);
MODULE_PARM_DESC(
	enable_platformprofile,
	"Enable the platform profile sysfs API to read and write the power mode.");

// TODO: remove this?
#define LEGIONFEATURES \
	"fancurve powermode platformprofile platformprofilenotify minifancurve fancurve_pmw_speed fancurve_rpm_speed"

//Size of fancurve stored in embedded controller
#define MAXFANCURVESIZE 10

#define LEGION_DRVR_SHORTNAME "legion"
#define LEGION_HWMON_NAME LEGION_DRVR_SHORTNAME "_hwmon"

struct legion_private;

/* =============================== */
/* Embedded Controller Description */
/* =============================== */

/* The configuration and registers to access the embedded controller
 * depending on different the version of the software on the
 * embedded controller or and the BIOS/UEFI firmware.
 *
 * To control fan curve in the embedded controller (EC) one has to
 * write to its "RAM". There are different possibilities:
 *  - EC RAM is memory mapped (write to it with ioremap)
 *  - access EC RAM via ported mapped IO (outb/inb)
 *  - access EC RAM via ACPI methods. It is only possible to write
 *    to part of it (first 0xFF bytes?)
 *
 * In later models the firmware directly exposes ACPI methods to
 * set the fan curve directly, without writing to EC RAM. This
 * is done inside the ACPI method.
 */

/**
 * Offsets for interesting values inside the EC RAM  (0 = start of
 * EC RAM) These might change depending on the software inside of
 * the EC, which can be updated by a BIOS update from Lenovo.
 */
// TODO: same order as in initialization
struct ec_register_offsets {
	// Super I/O Configuration Registers
	// 7.15 General Control (GCTRL)
	// General Control (GCTRL)
	// (see EC Interface Registers  and 6.2 Plug and Play Configuration (PNPCFG)) in datasheet
	// note: these are in two places saved
	// in EC Interface Registers  and in super io configuration registers
	// Chip ID
	u16 ECHIPID1;
	u16 ECHIPID2;
	// Chip Version
	u16 ECHIPVER;
	u16 ECDEBUG;

	// Lenovo Custom OEM extension
	// Firmware of ITE can be extended by
	// custom program using its own "variables"
	// These are the offsets to these "variables"
	u16 EXT_FAN_CUR_POINT;
	u16 EXT_FAN_POINTS_SIZE;
	u16 EXT_FAN1_BASE;
	u16 EXT_FAN2_BASE;
	u16 EXT_FAN_ACC_BASE;
	u16 EXT_FAN_DEC_BASE;
	u16 EXT_CPU_TEMP;
	u16 EXT_CPU_TEMP_HYST;
	u16 EXT_GPU_TEMP;
	u16 EXT_GPU_TEMP_HYST;
	u16 EXT_VRM_TEMP;
	u16 EXT_VRM_TEMP_HYST;
	u16 EXT_FAN1_RPM_LSB;
	u16 EXT_FAN1_RPM_MSB;
	u16 EXT_FAN2_RPM_LSB;
	u16 EXT_FAN2_RPM_MSB;
	u16 EXT_FAN1_TARGET_RPM;
	u16 EXT_FAN2_TARGET_RPM;
	u16 EXT_POWERMODE;
	u16 EXT_MINIFANCURVE_ON_COOL;
	// values
	// 0x04: enable mini fan curve if left for too long on cool level
	//      - this might be due to potential temp failure
	//      - or just because of really cool temps
	// 0xA0: disable it
	u16 EXT_LOCKFANCONTROLLER;
	u16 EXT_MAXIMUMFANSPEED;
	u16 EXT_WHITE_KEYBOARD_BACKLIGHT;
	u16 EXT_IC_TEMP_INPUT;
	u16 EXT_CPU_TEMP_INPUT;
	u16 EXT_GPU_TEMP_INPUT;
};

enum access_method {
	ACCESS_METHOD_NO_ACCESS = 0,
	ACCESS_METHOD_EC = 1,
	ACCESS_METHOD_ACPI = 2,
	ACCESS_METHOD_WMI = 3,
	ACCESS_METHOD_WMI2 = 4,
	ACCESS_METHOD_WMI3 = 5,
	ACCESS_METHOD_EC2 = 10, // ideapad fancurve method
	ACCESS_METHOD_EC3 = 11, // loq
	ACCESS_METHOD_EC4 = 12, // legion 2024 (e.g. 16IRX9)
};

// acpi paths used by this driver
enum acpi_paths_inventory_ids {
	ACPI_PATH_STA = 0, // _STA
	ACPI_PATH_CFG, // _CFG
	ACPI_PATH_READ_RAPIDCHARGE, // GBMD
	ACPI_PATH_WRITE_RAPIDCHARGE, // SBMC
	ACPI_PATH_READ_POWERMODE, // BTSM
	ACPI_PATH_READ_FANSPEED1, // FANS
	ACPI_PATH_READ_FANSPEED2, // FA2S
	ACPI_PATH_READ_CPU_TEMP, // CPUT
	ACPI_PATH_READ_GPU_TEMP, // GPUT
	ACPI_PATH_READ_FNLOCK, // HALS
	ACPI_PATH_WRITE_FNLOCK, // SALS
	ACPI_PATH_MAX // not a PATH just the max nbr of this enum
};

static const char *default_acpi_paths[ACPI_PATH_MAX] = {
	[ACPI_PATH_STA] = "_STA",
	[ACPI_PATH_CFG] = "_CFG",
	[ACPI_PATH_READ_RAPIDCHARGE] = "VPC0.GBMD",
	[ACPI_PATH_WRITE_RAPIDCHARGE] = "VPC0.SBMC",
	[ACPI_PATH_READ_POWERMODE] = "VPC0.BTSM",
	[ACPI_PATH_READ_FANSPEED1] = "FANS",
	[ACPI_PATH_READ_FANSPEED2] = "FA2S",
	[ACPI_PATH_READ_CPU_TEMP] = "CPUT",
	[ACPI_PATH_READ_GPU_TEMP] = "GPUT",
};

struct model_config {
	const struct ec_register_offsets *registers;
	bool check_embedded_controller_id;
	u16 embedded_controller_id;

	// first addr in EC we access/scan
	phys_addr_t memoryio_physical_ec_start;
	size_t memoryio_size;

	// TODO: maybe use bitfield
	bool has_minifancurve;
	bool has_custom_powermode;
	bool has_extreme_powermode;

	// Which EXT_*_TEMP_INPUT offsets have actually been validated for this
	// model, as a TEMP_REGISTER_* bitmask. Zero everywhere by default, which
	// keeps reading the hardcoded addresses below, exactly as before. Most
	// model configs carry unvalidated placeholders for these registers, so a
	// model may only opt in for the registers confirmed on real hardware.
	u8 validated_temp_registers;
	enum access_method access_method_powermode;

	enum access_method access_method_keyboard;
	enum access_method access_method_temperature;
	enum access_method access_method_fanspeed;
	enum access_method access_method_fancurve;
	enum access_method access_method_fanfullspeed;
	enum access_method access_method_powerlimits;
	bool three_state_keyboard;
	bool skip_ic_temp;
	bool skip_oc_controls;
	/* The lockfancontroller attribute writes EC byte
	 * EXT_LOCKFANCONTROLLER through Super-I/O port I/O regardless of
	 * the configured access methods; set on models whose EC does not
	 * declare that byte.
	 */
	bool skip_lockfancontroller;
	/*
	 * fan_maxspeed calls Fan_Get_MaxSpeed/Fan_Set_MaxSpeed (ids 3/4) on
	 * LENOVO_FAN_METHOD; set on firmware whose fan method does not
	 * implement them, where the call runs off the end of the dispatcher
	 * without a Return and succeeds, so the attribute reads a meaningless 0
	 * and a write is a no-op.
	 */
	bool skip_fan_maxspeed;
	bool acpi_fanspeed_is_rpm;
	/* fan_target registers hold duty-cycle (0-100); scale by 100 to approximate RPM */
	bool fan_target_is_duty;
	bool has_four_fans;
	/* hwmon fan3 is the firmware's fan 4, read through the WMI3 Other
	 * Method feature OtherMethodFeature_FAN_SPEED_4 (Q7CN).
	 */
	bool has_third_fan;
	/* Do not bind the GameZone WMI method block, so the mainline
	 * lenovo-wmi-gamezone driver can: lenovo-wmi-other needs it to read
	 * the power mode, else its firmware-attributes (PL1/PL2, cTGP, ...)
	 * return -EINVAL. Methods are still called by GUID.
	 */
	bool leave_gamezone_wmi_unbound;
	/* Re-apply the last fan table written through hwmon after resume: the
	 * EC raises every point to at least the mode default on resume (Q7CN),
	 * which silently loses quieter custom curves.
	 */
	bool restore_fancurve_on_resume;
	bool has_single_fan;
	u16 fan_max_rpm;
	bool fanfullspeed_requires_custom_powermode;
	bool skip_ylogo_light;
	bool skip_ioport_light;
	// EC register holding the Y-Logo light state; 0 = not available
	u16 ec_ylogo_register;

	bool acpi_check_dev;

	phys_addr_t ramio_physical_start;
	size_t ramio_size;
	const char *acpi_paths[ACPI_PATH_MAX];
	bool has_fancurve_defaults;
	bool wmi_fancurve_speed_only;
	bool require_unlocked_fan_controller;
	bool has_pl_coupling;
	/* Lift the firmware-imposed fan ceiling via WMAA(0, 0x0D, arg) on the
	 * GameZone WMI GUID. Only validated firmwares (e.g. KWCN54WW on the
	 * Legion Pro 7 16IRX8H) set this; on models without the sub-command the
	 * EC returns an ACPI error, so the sysfs attribute is hidden to avoid
	 * issuing an unverified WMI call. See issue #429 / PR #443.
	 */
	bool has_fan_unlock;
	bool has_fn_lock;
	bool has_flip_to_start;
	/* instant_boot_ac/instant_boot_usb_pd through WMI3 feature ids
	 * 0x03010001/0x03010002; set only where the DSDT implements both.
	 */
	bool has_instant_boot;
};

/* =================================== */
/* Configuration for different models */
/* =================================== */

// Idea by SmokelesssCPU (modified)
// - all default names and register addresses are supported by datasheet
// - register addresses for custom firmware by SmokelesssCPU
static const struct ec_register_offsets ec_register_offsets_v0 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC534,
	.EXT_FAN_POINTS_SIZE = 0xC535,
	.EXT_FAN1_BASE = 0xC540,
	.EXT_FAN2_BASE = 0xC550,
	.EXT_FAN_ACC_BASE = 0xC560,
	.EXT_FAN_DEC_BASE = 0xC570,
	.EXT_CPU_TEMP = 0xC580,
	.EXT_CPU_TEMP_HYST = 0xC590,
	.EXT_GPU_TEMP = 0xC5A0,
	.EXT_GPU_TEMP_HYST = 0xC5B0,
	.EXT_VRM_TEMP = 0xC5C0,
	.EXT_VRM_TEMP_HYST = 0xC5D0,
	.EXT_FAN1_RPM_LSB = 0xC5E0,
	.EXT_FAN1_RPM_MSB = 0xC5E1,
	.EXT_FAN2_RPM_LSB = 0xC5E2,
	.EXT_FAN2_RPM_MSB = 0xC5E3,
	.EXT_MINIFANCURVE_ON_COOL = 0xC536,
	.EXT_LOCKFANCONTROLLER = 0xc4AB,
	.EXT_CPU_TEMP_INPUT = 0xc538,
	.EXT_GPU_TEMP_INPUT = 0xc539,
	.EXT_IC_TEMP_INPUT = 0xC5E8,
	.EXT_POWERMODE = 0xc420,
	.EXT_FAN1_TARGET_RPM = 0xc600,
	.EXT_FAN2_TARGET_RPM = 0xc601,
	.EXT_MAXIMUMFANSPEED = 0xBD,
	.EXT_WHITE_KEYBOARD_BACKLIGHT = (0x3B + 0xC400)
};

/* RZCN: fan target bytes are duty-cycle (0-100) at 0xC5A0/0xC5A1,
 * not the power-limit setpoints at 0xC600/0xC601 used by v0.
 */
static const struct ec_register_offsets ec_register_offsets_rzcn = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC534,
	.EXT_FAN_POINTS_SIZE = 0xC535,
	.EXT_FAN1_BASE = 0xC540,
	.EXT_FAN2_BASE = 0xC550,
	.EXT_FAN_ACC_BASE = 0xC560,
	.EXT_FAN_DEC_BASE = 0xC570,
	.EXT_CPU_TEMP = 0xC580,
	.EXT_CPU_TEMP_HYST = 0xC590,
	.EXT_GPU_TEMP = 0xC5A0,
	.EXT_GPU_TEMP_HYST = 0xC5B0,
	.EXT_VRM_TEMP = 0xC5C0,
	.EXT_VRM_TEMP_HYST = 0xC5D0,
	.EXT_FAN1_RPM_LSB = 0xC5E0,
	.EXT_FAN1_RPM_MSB = 0xC5E1,
	.EXT_FAN2_RPM_LSB = 0xC5E2,
	.EXT_FAN2_RPM_MSB = 0xC5E3,
	.EXT_MINIFANCURVE_ON_COOL = 0xC536,
	.EXT_LOCKFANCONTROLLER = 0xC4AB,
	.EXT_CPU_TEMP_INPUT = 0xC538,
	.EXT_GPU_TEMP_INPUT = 0xC539,
	.EXT_IC_TEMP_INPUT = 0xC5E8,
	.EXT_POWERMODE = 0xC420,
	.EXT_FAN1_TARGET_RPM = 0xC5A0,
	.EXT_FAN2_TARGET_RPM = 0xC5A1,
	.EXT_MAXIMUMFANSPEED = 0xBD,
	.EXT_WHITE_KEYBOARD_BACKLIGHT = (0x3B + 0xC400)
};

static const struct ec_register_offsets ec_register_offsets_v1 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC534,
	.EXT_FAN_POINTS_SIZE = 0xC535,
	.EXT_FAN1_BASE = 0xC540,
	.EXT_FAN2_BASE = 0xC550,
	.EXT_FAN_ACC_BASE = 0xC560,
	.EXT_FAN_DEC_BASE = 0xC570,
	.EXT_CPU_TEMP = 0xC580,
	.EXT_CPU_TEMP_HYST = 0xC590,
	.EXT_GPU_TEMP = 0xC5A0,
	.EXT_GPU_TEMP_HYST = 0xC5B0,
	.EXT_VRM_TEMP = 0xC5C0,
	.EXT_VRM_TEMP_HYST = 0xC5D0,
	.EXT_FAN1_RPM_LSB = 0xC5E0,
	.EXT_FAN1_RPM_MSB = 0xC5E1,
	.EXT_FAN2_RPM_LSB = 0xC5E2,
	.EXT_FAN2_RPM_MSB = 0xC5E3,
	.EXT_MINIFANCURVE_ON_COOL = 0xC536,
	.EXT_LOCKFANCONTROLLER = 0xc4AB,
	.EXT_CPU_TEMP_INPUT = 0xc538,
	.EXT_GPU_TEMP_INPUT = 0xc539,
	.EXT_IC_TEMP_INPUT = 0xC5E8,
	.EXT_POWERMODE = 0xc41D,
	.EXT_FAN1_TARGET_RPM = 0xc600,
	.EXT_FAN2_TARGET_RPM = 0xc601,
	.EXT_MAXIMUMFANSPEED = 0xBD,
	.EXT_WHITE_KEYBOARD_BACKLIGHT = (0x3B + 0xC400)
};

static const struct ec_register_offsets ec_register_offsets_ideapad_v0 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC5a0, // not found yet
	.EXT_FAN_POINTS_SIZE = 0xC5a0, // constant 0
	.EXT_FAN1_BASE = 0xC5a0,
	.EXT_FAN2_BASE = 0xC5a8,
	.EXT_FAN_ACC_BASE = 0xC5a0, // not found yet
	.EXT_FAN_DEC_BASE = 0xC5a0, // not found yet
	.EXT_CPU_TEMP = 0xC550, // and repeated after 8 bytes
	.EXT_CPU_TEMP_HYST = 0xC590, // and repeated after 8 bytes
	.EXT_GPU_TEMP = 0xC5C0, // and repeated after 8 bytes
	.EXT_GPU_TEMP_HYST = 0xC5D0, // and repeated after 8 bytes
	.EXT_VRM_TEMP = 0xC5a0, // does not exists or not found
	.EXT_VRM_TEMP_HYST = 0xC5a0, // does not exists ot not found yet
	.EXT_FAN1_RPM_LSB = 0xC5a0, // not found yet
	.EXT_FAN1_RPM_MSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_LSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_MSB = 0xC5a0, // not found yet
	.EXT_MINIFANCURVE_ON_COOL = 0, // unsupported
	.EXT_LOCKFANCONTROLLER = 0, // unsupported
	.EXT_CPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_GPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_IC_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_POWERMODE = 0xC5a0, // not found yet
	.EXT_FAN1_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_FAN2_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_MAXIMUMFANSPEED = 0xC5a0, // not found yet
	.EXT_WHITE_KEYBOARD_BACKLIGHT = 0xC5a0 // not found yet
};

static const struct ec_register_offsets ec_register_offsets_ideapad_v1 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC5a0, // not found yet
	.EXT_FAN_POINTS_SIZE = 0xC5a0, // constant 0
	.EXT_FAN1_BASE = 0xC5a0,
	.EXT_FAN2_BASE = 0xC5a8,
	.EXT_FAN_ACC_BASE = 0xC5a0, // not found yet
	.EXT_FAN_DEC_BASE = 0xC5a0, // not found yet
	.EXT_CPU_TEMP = 0xC550, // and repeated after 8 bytes
	.EXT_CPU_TEMP_HYST = 0xC590, // and repeated after 8 bytes
	.EXT_GPU_TEMP = 0xC5C0, // and repeated after 8 bytes
	.EXT_GPU_TEMP_HYST = 0xC5D0, // and repeated after 8 bytes
	.EXT_VRM_TEMP = 0xC5a0, // does not exists or not found
	.EXT_VRM_TEMP_HYST = 0xC5a0, // does not exists ot not found yet
	.EXT_FAN1_RPM_LSB = 0xC5a0, // not found yet
	.EXT_FAN1_RPM_MSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_LSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_MSB = 0xC5a0, // not found yet
	.EXT_MINIFANCURVE_ON_COOL = 0, // unsupported
	.EXT_LOCKFANCONTROLLER = 0, // unsupported
	.EXT_CPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_GPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_IC_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_POWERMODE = 0xC5a0, // not found yet
	.EXT_FAN1_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_FAN2_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_MAXIMUMFANSPEED = 0xC5a0, // not found yet
	.EXT_WHITE_KEYBOARD_BACKLIGHT = 0xC5a0 // not found yet
};

static const struct ec_register_offsets ec_register_offsets_loq_v0 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC5a0,
	.EXT_FAN_POINTS_SIZE = 0xC5a0, // constant 0
	.EXT_FAN1_BASE = 0xcf02, // cpu rpm fan base
	.EXT_FAN2_BASE = 0xcf3e, // gpu rpm fan base
	.EXT_FAN_ACC_BASE = 0xC5a0, // not found yet
	.EXT_FAN_DEC_BASE = 0xC5a0, // not found yet
	.EXT_CPU_TEMP = 0xcf01, // cpu temp max base
	.EXT_CPU_TEMP_HYST = 0xcf00, // cpu temp min base
	.EXT_GPU_TEMP = 0xcf3d, // gpu temp max base
	.EXT_GPU_TEMP_HYST = 0xcf3c, // gpu temp min base
	.EXT_VRM_TEMP = 0xcf79, // IC max temp base
	.EXT_VRM_TEMP_HYST = 0xcf78, // IC min temp base
	.EXT_FAN1_RPM_LSB = 0xc509, // cpu fan base for reads
	.EXT_FAN1_RPM_MSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_LSB = 0xc530, // gpu fan base for reads
	.EXT_FAN2_RPM_MSB = 0xC5a0, // not found yet
	.EXT_MINIFANCURVE_ON_COOL = 0, // unsupported
	.EXT_LOCKFANCONTROLLER = 0, // unsupported
	.EXT_CPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_GPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_IC_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_POWERMODE = 0xc40a, // 3 bits from it
	.EXT_FAN1_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_FAN2_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_MAXIMUMFANSPEED = 0xC5a0, // not found yet
	.EXT_WHITE_KEYBOARD_BACKLIGHT = 0xC5a0 // not found yet
};

static const struct ec_register_offsets ec_register_offsets_loq_v1 = {
	.ECHIPID1 = 0x2000,
	.ECHIPID2 = 0x2001,
	.ECHIPVER = 0x2002,
	.ECDEBUG = 0x2003,
	.EXT_FAN_CUR_POINT = 0xC5a0,
	.EXT_FAN_POINTS_SIZE = 0xC5a0, // constant 0
	.EXT_FAN1_BASE = 0xcf02, // cpu rpm fan base
	.EXT_FAN2_BASE = 0xcf3e, // gpu rpm fan base
	.EXT_FAN_ACC_BASE = 0xC5a0, // not found yet
	.EXT_FAN_DEC_BASE = 0xC5a0, // not found yet
	.EXT_CPU_TEMP = 0xcf01, // cpu temp max base
	.EXT_CPU_TEMP_HYST = 0xcf00, // cpu temp min base
	.EXT_GPU_TEMP = 0xcf3d, // gpu temp max base
	.EXT_GPU_TEMP_HYST = 0xcf3c, // gpu temp min base
	.EXT_VRM_TEMP = 0xcf79, // ic max temp base
	.EXT_VRM_TEMP_HYST = 0xcf78, // ic min temp base
	.EXT_FAN1_RPM_LSB = 0xc509, // cpu fan base for reads
	.EXT_FAN1_RPM_MSB = 0xC5a0, // not found yet
	.EXT_FAN2_RPM_LSB = 0xc53c, // gpu fan base for reads
	.EXT_FAN2_RPM_MSB = 0xC5a0, // not found yet
	.EXT_MINIFANCURVE_ON_COOL = 0, // unsupported
	.EXT_LOCKFANCONTROLLER = 0, // unsupported
	.EXT_CPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_GPU_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_IC_TEMP_INPUT = 0xC5a0, // not found yet
	.EXT_POWERMODE = 0xc40a, // 3 bits from it
	.EXT_FAN1_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_FAN2_TARGET_RPM = 0xC5a0, // not found yet
	.EXT_MAXIMUMFANSPEED = 0xC5a0, // not found yet
	.EXT_WHITE_KEYBOARD_BACKLIGHT = 0xC5a0 // not found yet
};

static const struct model_config model_v0 = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.SBMC" }
};

static const struct model_config model_efcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	// Keyboard backlight on EFCN is handled by the mainline
	// ideapad-laptop driver through the VPC2004 device. The WMI
	// backlight GUID (8C5B9127-...) does not exist on this firmware
	// and the legacy GameZone methods 36/37 are not wired to the
	// physical light, so disable the LED here to avoid a failing
	// probe attempt at load time.
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600,
	// Y-Logo state register, reverse engineered on EFCN59WW: EC
	// value 0 = bright, 1 = dim, 2 = off (Fn+L cycles bright/dim/off
	// and the EC keeps this register in sync).
	.ec_ylogo_register = 0xC36F,
	.skip_ioport_light = true,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PCI0.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PCI0.LPCB.EC0.VPC0.SBMC" }
};

// Legion 5 Pro 16IAH7H (82RF, 2022), BIOS J2CN, EC chip 0x8227 (issue
// #600). Same register map as model_v0, but this Alder Lake DSDT puts
// the EC under \_SB.PC00.LPCB.EC0 instead of \_SB.PCI0.LPC0.EC0, so the
// v0 ACPI paths never resolve and rapidcharge stayed hidden. A duplicate
// J2CN allowlist entry using model_v0 shadowed this config until #601.
// Rapidcharge verified on BIOS J2CN35WW: charge power 79 W off vs
// 168 W avg / 188 W peak on, no fancurve/sensors/powermode regression.
static const struct model_config model_j2cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600,
	/* rapidcharge EC at \_SB.PC00.LPCB.EC0, not v0 \_SB.PCI0.LPC0.EC0 */
	.acpi_paths = { [ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" }
};

static const struct model_config model_9vcn = {
	.registers = &ec_register_offsets_ideapad_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8226,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_EC2,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_v2022 = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_4gcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8226,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_bvcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8226,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	/* BVCN firmware has no FAN_METHOD WMI data block
	 * (GUID 92549549-4bde-4f06-ac04-ce8bf898dbaa), so fan_fullspeed
	 * has no backend here and only errors with ENODEV (issue #94)
	 */
	.access_method_fanfullspeed = ACCESS_METHOD_NO_ACCESS,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFC7E0800,
	.ramio_size = 0x600
};

static const struct model_config model_bhcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8226,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = false,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_ACPI,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_ACPI,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFF00D400,
	.ramio_size = 0x600
};

static const struct model_config model_kwcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	/*
	 * KWCN54WW (Legion Pro 5 16IRX8, 82WK) declares only Fan_Get_Table and
	 * Fan_Set_Table on LENOVO_FAN_METHOD, only CPU_Set_OC_Data on
	 * LENOVO_CPU_METHOD and nothing on LENOVO_GPU_METHOD, so the fan
	 * full-speed methods (ids 1/2), the max-speed methods (ids 3/4) and
	 * the legacy power-limit methods cannot work. LENOVO_OTHER_METHOD
	 * answers for the plain feature IDs and LENOVO_CAPABILITY_DATA_01 /
	 * LENOVO_DISCRETE_DATA publish the per-mode ranges, so use the Other
	 * Method paths (writes are clamped to the capability-data ranges).
	 */
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_fan_maxspeed = true,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	/*
	 * Fan_Set_Table carries one level per point for both fans; the pwm2,
	 * temperature and accel/decel curve attributes have no effect, so
	 * hide them as model_n2cn does. Level 10 was measured at 5400 RPM,
	 * which the driver now reports from the fan table data itself.
	 */
	.wmi_fancurve_speed_only = true,
	/*
	 * LENOVO_FAN_TABLE_DATA carries one RPM ladder per fan (fan 1 /
	 * sensor 0x04 and fan 2 / sensor 0x05), identical in every power
	 * mode; exposes fan1_level_rpm_table/fan2_level_rpm_table.
	 */
	.has_fancurve_defaults = true,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600,
	/* 82WK DSDT: _CFG, GBMD and SBMC live under VPC0, not on the EC. */
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	/* WMAA(0, 0x0D, 0x01) raises the firmware fan ceiling from ~4400 RPM to
	 * ~7000-7100 RPM on KWCN54WW (Legion Pro 7 16IRX8H, EC 0x5507).
	 * Shared with the Pro 5 16IRX8 (82WK): validated only on the 16IRX8H
	 * sibling; on the 82WK fan_unlock was not exercised, and the level
	 * table caps the fans at 5400 RPM regardless.
	 */
	.has_fan_unlock = true
};

static const struct model_config model_g8cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_n2cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.wmi_fancurve_speed_only = true,
	.require_unlocked_fan_controller = true
};

static const struct model_config
	model_nmcn = { .registers = &ec_register_offsets_v0,
		       .check_embedded_controller_id = true,
		       .embedded_controller_id = 0x5507,
		       .memoryio_physical_ec_start = 0xC400,
		       .memoryio_size = 0x300,
		       .has_minifancurve = false,
		       .has_custom_powermode = true,
		       .access_method_powermode = ACCESS_METHOD_WMI,
		       .access_method_keyboard = ACCESS_METHOD_WMI,
		       .access_method_fanspeed = ACCESS_METHOD_WMI3,
		       .access_method_temperature = ACCESS_METHOD_WMI3,
		       .access_method_fancurve = ACCESS_METHOD_EC4,
		       .access_method_fanfullspeed = ACCESS_METHOD_EC4,
		       .access_method_powerlimits = ACCESS_METHOD_WMI3,
		       .acpi_check_dev = false,
		       .ramio_physical_start = 0xFE0B0400,
		       .ramio_size = 0x600,
		       .acpi_paths = {
			       /* rapidcharge EC at \_SB.PC00.LPCB.EC0, not v0 \_SB.PCI0.LPC0.EC0 */
			       [ACPI_PATH_READ_RAPIDCHARGE] =
				       "\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			       [ACPI_PATH_WRITE_RAPIDCHARGE] =
				       "\\_SB.PC00.LPCB.EC0.VPC0.SBMC",
		       } };

static const struct model_config model_nxcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5263,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_m0cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_m1cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_m2cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_m6cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_k1cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5263,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_rzcn = {
	.registers = &ec_register_offsets_rzcn,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5263,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanspeed = ACCESS_METHOD_ACPI,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_NO_ACCESS,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.acpi_fanspeed_is_rpm = true,
	.fan_target_is_duty = true,
	.has_four_fans = true,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_nscn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	// not implemented (properly) in WMI, RGB conrolled by USB
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	// accessing fan speed is not implemented in ACPI
	// a variable in the operation region (or not found)
	// and not per WMI (methods returns constant 0)
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

// Legion 7 16IRX9 (83FD) - 2024, Intel Core i9-14900HX + RTX 4070
// BIOS: NSCN37WW, EC 0x5507 (issue #617). Facts below are from the
// NSCN37WW SSDT4 ("CB-01") attached in issue #617; the WMA* methods
// live in \_SB.GZFD:
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (GFAN/SFAN), so the WMAB 1/2 full-speed path of model_nscn and the
//   max-speed methods 3/4 cannot work. GFAN returns the ten EC bytes
//   F101..F10A (EFAN @0xFE0B0F00 + 0xE0), a static 1..10 placeholder in
//   extreme mode (GZ44 == 7), and reinitialises the table via INIF when
//   F10A is 0. SFAN reads the mode byte F000 (1/2/3/0xFF/0xE0) to pick
//   an FNT0 row and writes CRP/GRP/ERP = FNT[level + 2] / 100 for all
//   fans; mode 0 leaves that row unset. WMAB 6 drops writes while
//   GZ44 == 7. The bytes are fan LEVELS 1..10 (FNT0 level 1 is 0 RPM),
//   one table for all fans: the same GFAN/SFAN semantics as model_t2cn,
//   so the payload mode and the RPM calibration use the live thermal
//   mode (GameZone 0x37 reads GZ44; 0x2D reads a saved request).
// - LENOVO_FAN_TABLE_DATA is WQA7 -> SFTW (15 FNT0 rows, fan 1/sensor 4
//   and fan 2/sensor 5), so the per-level RPM ladders are available.
// - Other Method WMAE: fan RPM DEV 4 FEA 3 (FA1S/FA2S * 100), full speed
//   DEV 4 FEA 2 -> EC FFON (get and set), CPU/GPU temps DEV 5 FEA 4/5;
//   DEV 5 FEA 1 (IC temp) is a constant 0 -> skip_ic_temp.
// fan_fullspeed is gated behind custom power mode like model_m3cn_8227/
// model_q7cn. Product-qualified so other NSCN units keep model_nscn.
static const struct model_config model_nscn_83fd = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	// RGB keyboard is controlled over USB, as on model_nscn
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_fan_maxspeed = true,
	.skip_lockfancontroller = true,
	.wmi_fancurve_speed_only = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

static const struct model_config model_qncn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_lpcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600,
	.acpi_paths = {
		[ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
		[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
		[ACPI_PATH_READ_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
		[ACPI_PATH_WRITE_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.SBMC",
	},
	/* max-power (the "extreme" platform_profile choice) cuts power
	 * instantly on this EC (hard power-off, no shutdown sequence) instead
	 * of raising limits like on other has_extreme_powermode models, so
	 * keep it out of platform_profile's choices entirely.
	 */
	.has_extreme_powermode = false,
	/* WMI3 fancurve writes only transmit per-point speed1 (see
	 * wmi_write_fancurve_custom): temperature thresholds, fan2 speed
	 * and accel/decel have no WMI backing and never persist. Expose
	 * only the speed attributes so userspace does not write (and the
	 * GUI/CLI does not crash on) fields the firmware drops.
	 */
	.wmi_fancurve_speed_only = true
};

static const struct model_config model_kfcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_hacn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_k9cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400, // or replace 0xC400 by 0x0400  ?
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_eucn = {
	.registers = &ec_register_offsets_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_fccn = {
	.registers = &ec_register_offsets_ideapad_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_ACPI,
	.access_method_fancurve = ACCESS_METHOD_EC2,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_h3cn = {
	//0xFE0B0800
	.registers = &ec_register_offsets_ideapad_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = false,
	.access_method_powermode = ACCESS_METHOD_WMI,
	// not implemented (properly) in WMI, RGB conrolled by USB
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	// accessing fan speed is not implemented in ACPI
	// a variable in the operation region (or not found)
	// and not per WMI (methods returns constant 0)
	.access_method_fanspeed = ACCESS_METHOD_NO_ACCESS,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0800,
	.ramio_size = 0x600
};

static const struct model_config model_e9cn = {
	//0xFE0B0800
	.registers = &ec_register_offsets_v1,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400, //0xFC7E0800
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = false,
	.access_method_powermode = ACCESS_METHOD_WMI,
	// not implemented (properly) in WMI, RGB conrolled by USB
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	// accessing fan speed is not implemented in ACPI
	// a variable in the operation region (or not found)
	// and not per WMI (methods returns constant 0)
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFC7E0800,
	.ramio_size = 0x600
};

static const struct model_config model_8jcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8226,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600
};

static const struct model_config model_jncn = {
	.registers = &ec_register_offsets_v1,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = false,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanspeed = ACCESS_METHOD_WMI,
	.access_method_temperature = ACCESS_METHOD_WMI,
	.access_method_fancurve = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFC7E0800,
	.ramio_size = 0x600
};

// Yoga Model!
static const struct model_config model_j1cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

// Yoga Model!
static const struct model_config model_dmcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE700D00,
	.ramio_size = 0x600
};

// Yoga Model!
static const struct model_config model_khcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_EC,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_EC,
	.access_method_temperature = ACCESS_METHOD_EC,
	.access_method_fancurve = ACCESS_METHOD_EC,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

// LOQ Model
static const struct model_config model_lzcn = {
	.registers = &ec_register_offsets_loq_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true
};

// LOQ Model 2024
static const struct model_config model_necn = {
	.registers = &ec_register_offsets_loq_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true
};

// LOQ 15AHP9
static const struct model_config model_nzcn = {
	.registers = &ec_register_offsets_loq_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG" },
	.has_fancurve_defaults = true
};

// LOQ 15AHP10
static const struct model_config model_r8cn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG" },
	.has_fancurve_defaults = true
};

// Legion 5 15AHP11 (83Q7, AMD Ryzen 7 250 + RTX 5050) - same platform as
// LOQ 15AHP10, BIOS T2CN33WW, EC chip 0x5509 (issue #504).
// EC port I/O is blocked by the firmware (vendor EC RAM reads return 0).
// The DSDT supplied in #504 confirms WMAB 5/6 -> GFAN/SFAN: ten shared
// LEVEL indices into FNT0/FNT1 (SFAN indexes level + 2). WQA7/SFTW exports
// that same ladder, including level 1's zero RPM; never shift it away.
// SFAN requires a valid powermode byte and WMAB drops writes in extreme
// mode (GZ44 == 7). WMI3 sensors/powermode and curve reads are verified;
// corrected curve writes still need an on-hardware round trip.
// The 83Q7 has no Y-logo/lid and no IO-port light.
// The minifancurve EC register never holds a valid value on this unit
// (reads 0 or 8, never 0x04/0xA0), so the feature is disabled. Keyboard
// backlight brightness calls fail on both the WMI and WMI2 paths
// (firmware returns 0), so keyboard backlight is disabled too; Fn+Space
// still works at firmware level.
static const struct model_config model_t2cn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5509,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG" },
	.has_fancurve_defaults = true
};

// Legion Pro 5 16AFR10 (83F2, AMD Ryzen 9 9955HX + RTX 5070), BIOS RECN,
// EC chip 0x5508 (issue #527) - same EC generation as model_secn/model_s2cn.
// EC fan curve reads return size 0 / all zeros on this EC generation while
// the WMI fan curve table is populated, so fancurve uses WMI3; WMI3
// temps/fans, WMI powermode and custom powermode confirmed on the unit
// in #527. Minifancurve has no mapped LOQ control register. ACPI CFG and rapidcharge (GBMD/SBMC) work via the
// PCI0.LPC0 paths. The 83F2 has no Y-logo/lid and no IO-port light
// (reporter-confirmed; the phantom WMI lid-light probe returns 1).
// The WMI fan table is the level-index kind shared across the 0x5508
// generation (issue #491: WMAB ids 5/6 only, EC F9F0..F9F9 levels, no
// temperature fields - see model_q7cn), so only the speed attributes are
// exposed and FAN_SPEED_UNIT_LEVEL applies.
static const struct model_config model_recn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true
};

// Legion Slim 5 16AHP9 (2024) - Model 83DH
static const struct model_config model_nrcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600
};

// LOQ 15IRX10 (83JE) - 2025, Intel + RTX 50, BIOS R3CN (issues #374/#535).
// Keep the EC3 LOQ curve: independent fan RPM and temperature/hysteresis
// writes were verified in custom mode (255) on R3CN44WW under load (#535).
// EC chip id 0x5508 alone does not imply the Legion EC layout is in use.
// ec_register_offsets_loq_v1 supplies this model's existing register map;
// accel/decel are not stored by the EC3 interface and must stay hidden.
//
// The GZFD WMI device is in SSDT4.dsl from R3CN.zip (#374), not the DSDT.
// WMAB 5/6 (L19638) delegate to GFAN/SFAN (L15463/L15534): the ten values
// in F101..F10A are level indices shared by both fans. SFAN derives RPM
// and temperatures from FNT0..FNT2, writes the EFAN fields (L1661) and
// ignores writes in extreme mode. This is NOT equivalent to EC3's RPM
// curve. Keep FAN_SPEED_UNIT_LEVEL only for the WMI debugfs readout;
// fan1/fan2_level_rpm_table must not be exposed for the active EC3 curve.
// WQA3 (L18101) provides firmware ladders; has_fancurve_defaults permits
// restoring them via SFAN, whose first payload byte must name the mode.
//
// WMAE Get(0x11)/Set(0x12) (L19673/L20503) uses DEV0/FEA0/TYP0 dispatch:
// RPM 0x04030001/2 -> FA1S/FA2S * 100; full speed 0x04020000 -> FFON;
// CPU temp 0x05040000 -> CTMP; GPU temp 0x05050000 -> SKTC (socket).
// 0x05010000 returns Zero, so hide the IC sensor. PL/OC -> CSPL/CLPL/
// CCTL/CCPL. WMI3 sensors and WMAA powermode were verified in #374;
// bad EC sensor reads do not invalidate the separately mapped EC3 curve.
// Keep fan_fullspeed restricted to custom mode (0x5508 wedge risk).
// KBBACKLIGHT WMAF drives the keyboard; read-only ecmemoryram uses the
// EFAN window at 0xFE0B0F00. Minifancurve and lockfancontroller have only
// placeholder offsets in loq_v1, so never expose those writes.
static const struct model_config model_r3cn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_lockfancontroller = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG" },
	.has_fancurve_defaults = true,
};

// Legion 7 16IAX10 (83KY) - 2025, Intel Arrow Lake + RTX 5060
// BIOS: RXCN79WW, EC chip: 0x5508
// Uses WMI3 for all fan operations (safe - no direct EC memory writes).
// The WMI fan table behaves like the rest of the 0x5508 generation (issue
// #491): per-point speed1 only, temperature columns always read 0, EC
// values matching the F9F0..F9F9 level bytes - i.e. the level-index kind
// (FAN_SPEED_UNIT_LEVEL, see model_q7cn). Only the speed attributes are
// exposed (wmi_fancurve_speed_only); writing percent values into the
// level bytes matches the thermal-shutdown reports for this generation.
static const struct model_config model_rxcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = false,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600,
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true
};

// Legion 5 16IAX10 (83NX) - 2025, Intel Arrow Lake HX + RTX 5060 Max-Q
// BIOS: Q6CN32WW, EC chip: 0x5508 (confirmed on-hardware)
// Same chassis generation/EC id/ramio window as model_rxcn (Legion 7
// 16IAX10, 83KY); on-hardware testing showed raw EC and ACPI fan/temp
// reads return garbage (fan RPM exceeding the reported max, EC values
// unrelated to actual sensors) while WMI3 reads back correct values, so
// this uses WMI3 for all fan/temp/power-limit operations like model_rxcn.
// Also enables WMI3 power limits (cpu_temperature_limit/cpu_l1_tau/
// gpu_power_target_offset), confirmed returning plausible values on the
// unit, same as model_q8cn/model_nmcn/model_lpcn/model_lzcn.
// The WMI fan table behaves like the rest of the 0x5508 generation (issue
// #491): per-point speed1 only, temperature columns always read 0, EC
// values matching the F9F0..F9F9 level bytes - i.e. the level-index kind
// (FAN_SPEED_UNIT_LEVEL, see model_q7cn). Only the speed attributes are
// exposed (wmi_fancurve_speed_only); writing percent values into the
// level bytes matches the thermal-shutdown reports for this generation.
static const struct model_config model_q6cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE00D400,
	.ramio_size = 0x600,
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true
};

// Legion Pro 5 16IAX10H (83LU) - 2025, Intel Arrow Lake-HX + RTX 5070 Ti
// BIOS: Q6CN26WW (issue #337), EC chip 0x5508 (read on the unit, fw 2b0).
// The Q6CN BIOS prefix is shared with the Legion 5 16IAX10 (83NX) above,
// but this is the Legion Pro 5 chassis; the DSDT (L = dsdt.dsl line, issue
// #337) shows the same WMI-only firmware layout as model_q7cn (Legion Pro
// 7 16IAX10H, 83F5, the Q7CN-BIOS sibling of this chassis):
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L55540): get returns the 0x58-byte LFGT buffer with the live EC
//   fields F9F0..F9F9 (static 1..10 placeholder when ODV1 == 4, i.e.
//   extreme mode); set copies the ten speeds (bytes 0x06..0x18 at even
//   offsets) to F9F0..F9F9 and commits via LECR(0xD0,1,1,2), no range
//   check, mode/FSID bytes ignored. The bytes are fan LEVELS 1..10
//   (LENOVO_FAN_TABLE_DATA, instance A3, is present in the _WDG), one
//   table for all fans, temperature axis fixed by the EC, hence
//   FAN_SPEED_UNIT_LEVEL and only the speed attributes are exposed
//   (wmi_fancurve_speed_only).
// - Other Method WMAE Get(17)/Set(18) (L55623/L56286) implements the
//   standard feature ids: fan RPM 0x04030001/2 (FANS*0x64), CPU/GPU temp
//   0x05040000/0x05050000 (CPUT/GPUT), full speed 0x04020000 -> EC FNST
//   (both directions), PL/OC 0x0101..0x0107/0x0201..0x0204 stored raw in
//   the EC. 0x05010000 is the CPU socket temp (CPUS), not a labeled IC
//   sensor -> skip_ic_temp.
// - Power mode: GameZone WMAA SmartFanMode set 0x2C / get 0x2D
//   (L54931/L54802): quiet/balanced/performance, 0xFF custom, 0xE0
//   extreme; requests are parked on battery (ACTY() checks).
// - CPU Method WMAC is an empty stub (L55617), so power limits go
//   through WMAE (WMI3), as on model_q7cn/model_rlcn.
// - Keyboard backlight (white, off/medium/bright) is driven by the
//   KBBACKLIGHT WMAF get(1)/set(2) (L57234/L57288, LECR 0xDA func 3,
//   levels 1..3); light ids 0x03/0x04 exist in the DSDT but the chassis
//   has no Y-logo or IO-port lights (issue #337 report), so both are
//   skipped.
// - EC RAM window ERAX @0xFE500400 (L37121, len 0xFF; F9FT/ECB2 at
//   +0x100/+0x200, ramio_size 0x300) is used only by the read-only
//   debugfs ecmemoryram dump; EC register offsets are not trusted on the
//   0x5508 generation (issue #491) - everything else goes through WMI.
// - Rapid charge via VPC0 GBMD/SBMC (L39548/L39765); SBMC(7) also
//   clears conservation mode.
// Runtime validation on the reporter's unit (issue #337, tramp-tm):
// legion_wmi_other reports plausible fan RPM (2400/2100) while raw EC and
// ACPI-path reads are garbage (e.g. 18045 RPM with the fans at ~2400),
// matching the EC-misalignment quirk of this generation; dmesg with
// force=1 shows EC id 0x5508 fw 2b0 and all five WMI blocks (GameZone,
// Fan Method, Other Method, CPU Method, KBBACKLIGHT) registering.
// fan_fullspeed is gated behind custom power mode like model_q7cn/
// model_rlcn: on this generation a full-speed write through the wrong
// path can wedge the fans at maximum speed until reboot (model_rlcn).
static const struct model_config model_q6cn_lu = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE500400,
	.ramio_size = 0x300,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
};

// Legion Pro 5 16IAX10 (83F3) - 2025, Intel Core Ultra 7 255HX + RTX 5070
// BIOS: Q6CN78WW/Q6CN79WW (issue #471), EC 0x5508. Facts below are from
// the Q6CN79WW DSDT (L = dsdt.dsl line, attachment in issue #471); the
// WMA* methods live in \_SB.GZFD and the chassis is the Legion Pro 5
// sibling of the 83LU (model_q6cn_lu, same Q6CN BIOS line):
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L59967/L60012): get returns the 0x58-byte LFGT buffer with the live
//   EC fields F9F0..F9F9 (static 1..10 placeholder when ODV1 == 4, i.e.
//   extreme mode, L59983); set copies the ten speeds (bytes 0x06..0x18
//   at even offsets) to F9F0..F9F9 and commits via LECR(0xD0,1,1,2)
//   (L60037), no range check, mode/FSID bytes ignored. The bytes are fan
//   LEVELS 1..10, one table for all fans, temperature axis fixed by the
//   EC, hence FAN_SPEED_UNIT_LEVEL and only the speed attributes are
//   exposed (wmi_fancurve_speed_only). Read on hardware: 1,2,3,4,5,6,
//   7,8,8,8 (performance mode, issue #471).
// - LENOVO_FAN_TABLE_DATA (WQA3, L46178) maps level 1..10 to
//   1700..5300 RPM on fan 1 (sensor 0x04) and fan 2 (sensor 0x05), so
//   fan1_level_rpm_table/fan2_level_rpm_table appear automatically
//   (has_fancurve_defaults); the _WDG carries both the Fan Method and
//   the KBBACKLIGHT (offset 0x50) data blocks.
// - Other Method WMAE Get(0x11)/Set(0x12) (L60048/L60711) implements the
//   standard feature ids: fan RPM 0x04030001/2 (FANS*0x64), CPU/GPU temp
//   0x05040000/0x05050000, full speed 0x04020000 -> EC FNST (both
//   directions), PL/OC 0x0101..0x0108/0x0201..0x0204 stored raw in the
//   EC. 0x05010000 is the CPU socket temp (CPUS), not a labeled IC
//   sensor -> skip_ic_temp. PL/OC attributes stay hidden
//   (skip_oc_controls) until validated; only cpu_temperature_limit,
//   cpu_l1_tau and gpu_power_target_offset are visible.
// - Power mode: GameZone WMAA SmartFanMode set 0x2C / get 0x2D
//   (L59356/L59227): quiet/balanced/performance, 0xFF custom, 0xE0
//   extreme. CPU Method WMAC is an empty stub (L60042), so power
//   limits go through WMAE (WMI3).
// - Keyboard backlight (4-zone RGB; the WMI path drives the on/off and
//   brightness levels, effects stay userspace) via KBBACKLIGHT WMAF
//   get(1)/set(2) (L61749/L61803, LECR 0xDA); the chassis has no
//   Y-logo or IO-port lights (issue #471 report), so both are skipped.
// - EC0 at \_SB.PC00.LPCB.EC0 (L36695, in Scope (\_SB.PC00.LPCB)
//   L36585, _STA L36731); EC RAM window ERAX @0xFE500400 (L36982,
//   len 0xFF; F9FT/ECB2 at +0x100/+0x200, ramio_size 0x300) is used
//   only by the read-only debugfs ecmemoryram dump; EC register
//   offsets are not trusted on the 0x5508 generation (issue #491) -
//   everything else goes through WMI. VPC0 (VPC2004) _STA/_CFG
//   (L39301/L39306), GBMD/SBMC (L39468/L39685).
// Runtime reads on the reporter's unit (issue #471, inermage): EC
// column is garbage (80/87 C, 18045/16743 RPM) while ACPI and WMI3
// agree (CPU 64 C, fans 2200 RPM), EC id 0x5508 fw 2b0, powermode
// WMI 3 - matching the EC-misalignment quirk of this generation.
// fan_fullspeed is gated behind custom power mode like model_q7cn/
// model_rlcn: on this generation a full-speed write through the wrong
// path can wedge the fans at maximum speed until reboot (model_rlcn).
static const struct model_config model_q6cn_f3 = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE500400,
	.ramio_size = 0x300,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
};

// Legion 5 15IAX10 (83F0) - same EC Chip ID (0x5508) as R3CN (LOQ 15IRX10)
// EC3 fancurve reads return garbage on this chassis (#359/#475), and the
// DSDT disassembly in issue #491 (Silver-Rust-18) shows why the WMI path
// is the right one: Fan Method WMAB implements only Fan_Get_Table(5)/
// Fan_Set_Table(6) with the 0x58-byte LFGT buffer - ten fan LEVELS 1..10
// in EC F9F0..F9F9, committed via LECR(0xD0,1,1,2), no temperature fields
// anywhere (same layout as model_q7cn/model_rlcn/model_recn). The get
// branch returns a static 1..10 placeholder while ODV1 == 4 (set by the
// ASMC state machine, e.g. the extreme thermal mode), so read the table
// in another mode. WMI3 temps/fans/powermode and monitoring are confirmed
// on the unit (#475, gauthier1024 in #491); the EC3 offsets borrowed from
// model_r3cn are wrong for this chassis and unsafe to write.
static const struct model_config model_s2cn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true
};

static const struct model_config model_m3cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5507,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600,
	.acpi_paths = {
		[ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
		[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
		[ACPI_PATH_READ_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
		[ACPI_PATH_WRITE_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.SBMC",
	},
	.has_extreme_powermode = true
};
static const struct model_config model_m3cn_8227 = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.acpi_check_dev = true,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x600,
	.acpi_paths = {
		[ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
		[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
		[ACPI_PATH_READ_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
		[ACPI_PATH_WRITE_RAPIDCHARGE] = "\\_SB.PCI0.LPC0.EC0.VPC0.SBMC",
	},
	/* max-power (the "extreme" platform_profile choice) cuts power
	 * instantly on this EC (hard power-off, no shutdown sequence) instead
	 * of raising limits like on other has_extreme_powermode models, so
	 * keep it out of platform_profile's choices entirely.
	 */
	.has_extreme_powermode = false,
	/* Fan_Set_Table carries only per-point speeds on this EC (0x8227);
	 * temperature thresholds, fan2 speed and accel/decel have no WMI
	 * backing, so hide them like model_lpcn does (issue #582).
	 */
	.wmi_fancurve_speed_only = true,
	.skip_lockfancontroller = true,
};
// LOQ 15IAX9E
static const struct model_config model_q8cn = {
	.registers = &ec_register_offsets_loq_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x8227,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B05C0,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG" },
	.has_fancurve_defaults = false
};

static const struct model_config model_secn = {
	.registers = &ec_register_offsets_loq_v1,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = true,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_EC3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0F00,
	.ramio_size = 0x600,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC",
			[ACPI_PATH_READ_FNLOCK] =
				"\\_SB.PC00.LPCB.EC0.VPC0.HALS",
			[ACPI_PATH_WRITE_FNLOCK] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SALS" },
	.has_fancurve_defaults = true,
	.has_pl_coupling = true,
	.has_fan_unlock = true,
	.has_fn_lock = true,
	.has_flip_to_start = true,
	.has_single_fan = true,
	.fan_max_rpm = 5400,
	.fanfullspeed_requires_custom_powermode = true,
};

// Legion 5i Gen 10 (83VK) - 2025/2026, Intel Arrow Lake-HX + RTX 5060
// BIOS: UFCN27WW, EC chip: 0x5509
// Fan speed/temperature reads and powermode validated on real hardware (issue #507).
// Fancurve writes currently fail in the firmware's \_SB.GZFD.SFAN method
// (AE_AML_BUFFER_LIMIT), so WMI3 fancurve access is read-only on this BIOS.
static const struct model_config
	model_ufcn = { .registers = &ec_register_offsets_v0,
		       .check_embedded_controller_id = true,
		       .embedded_controller_id = 0x5509,
		       .memoryio_physical_ec_start = 0xC400,
		       .memoryio_size = 0x300,
		       .has_minifancurve = true,
		       .has_custom_powermode = true,
		       .has_extreme_powermode = true,
		       .access_method_powermode = ACCESS_METHOD_WMI,
		       .access_method_keyboard = ACCESS_METHOD_WMI,
		       .access_method_fanspeed = ACCESS_METHOD_WMI3,
		       .access_method_temperature = ACCESS_METHOD_WMI3,
		       .access_method_fancurve = ACCESS_METHOD_WMI3,
		       .access_method_fanfullspeed = ACCESS_METHOD_WMI,
		       .acpi_check_dev = false,
		       .ramio_physical_start = 0xFE0B0400,
		       .ramio_size = 0x600,
		       .acpi_paths = {
			       // rapidcharge EC at \_SB.PC00.LPCB.EC0, not v0 \_SB.PCI0.LPC0.EC0
			       [ACPI_PATH_READ_RAPIDCHARGE] =
				       "\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			       [ACPI_PATH_WRITE_RAPIDCHARGE] =
				       "\\_SB.PC00.LPCB.EC0.VPC0.SBMC",
		       } };

// Legion Pro 7 16IAX10H (83F5) - 2025, Intel Arrow Lake-HX + RTX 50
// BIOS: Q7CN78WW, EC firmware 1.78. Facts below are from this unit's DSDT
// (L = DSDT.dsl line); the EC-internal 0xC4xx offsets are only inferred.
// - EC RAM window ERAX @0xFE500400 (L37243, plus F9FT/ECB2 at +0x100/
//   +0x200, ramio_size 0x300) is used only by debugfs ecmemoryram; it is
//   byte-identical to the EC I2EC bank at 0xC400 and to the ACPI EC I/O
//   space (verified on hardware). Nothing writes EC RAM, so
//   lockfancontroller/minifancurve (undeclared bytes) are hidden.
// - Power mode: GameZone WMAA 0x2C set / 0x2D get (L61539-61800):
//   1 quiet, 2 balanced, 3 performance, 0xFF custom, 0xE0 extreme.
//   custom/performance/extreme need AC; on battery the request is
//   parked and read back as if applied.
// - Fan table: Fan Method WMAB has only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L62277-62351); set copies the byte at each even offset 0x06..0x18
//   of the 0x40-byte buffer to EC F9F0..F9F9 and calls LECR(0xD0), no
//   range check. The bytes are fan LEVELS 1..10 (LENOVO_FAN_TABLE_DATA
//   WQA3, L46562-47779: level 1 = per-fan minimum RPM, 10 = maximum; one
//   table shared by all fans; the temperature axis is fixed by the EC;
//   level 0 is undocumented), hence FAN_SPEED_UNIT_LEVEL for this model.
//   Fan_Get_Table returns a static 1..10 placeholder in extreme mode
//   (ODV1 == 4) instead of the live table, and Fan_Set_Table ignores the
//   mode byte. Read on hardware:
//   1,2,3,4,5,6,7,8,8,8 (performance mode).
// - RPM/temps/full speed: Other Method WMAE Get(17)/SetFeatureValue(18)
//   ids 0x04030001/0x04030002 (RPM), 0x05040000/0x05050000 (CPU/GPU
//   temp, L62942-62965), 0x04020000 (full speed, L62877-62888).
// - Power limits: CPU Method WMAC is an empty stub (L62354), so WMAE via
//   WMI3 (writes are clamped to the capability-data ranges). WMAE stores 0x0101/0x0102/0x0104/0x0106 and
//   0x0201..0x0204 raw in the EC (get L62534-62690, set L63327-63600);
//   0x0103 always reads back 0, 0x0105 is unimplemented and 0x0107 (tau)
//   goes through a 13-entry lookup. skip_oc_controls hides the PL/OC
//   attributes; only cpu_temperature_limit, cpu_l1_tau and
//   gpu_power_target_offset stay visible.
// - Keyboard is USB-HID ITE 048d:c197 "Spectrum" that the WMI light
//   methods do not drive -> NO_ACCESS; Y-logo / IO-port light skipped.
// - EC id 0x5508 (version 2b0) read on this unit via Super-I/O port I/O;
//   also reported for other 83F5 units (issue #385).
// - Rapid charge via VPC0 GBMD/SBMC (L39816/L40033); SBMC(7) also
//   clears conservation mode.
static const struct model_config model_q7cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_NO_ACCESS,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE500400,
	.ramio_size = 0x300,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	/*
	 * LENOVO_FAN_TABLE_DATA (WQA3, switched on GSKU) carries one RPM
	 * ladder per fan, identical in every power mode: fan 1 / sensor
	 * 0x01 (CPU fan), fan 2 / sensor 0x05 (GPU fan) and fan 4 / sensor
	 * 0x04 (the small third fan, hwmon fan3; its ladder is not
	 * exposed). There is no (fan 1, sensor 0x04) row, so
	 * fan1_level_rpm_table comes from the sensor 0x01 row.
	 */
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	/* WMAE Get 0x04030004 returns EC FASF * 100 (fan 4 RPM) */
	.has_third_fan = true,
	/* PL/OC attributes are hidden (skip_oc_controls) in favour of the
	 * lenovo-wmi-other firmware-attributes, which need GameZone bound
	 * by lenovo-wmi-gamezone.
	 */
	.leave_gamezone_wmi_unbound = true,
	/* the EC floors the fan table at the mode default on resume */
	.restore_fancurve_on_resume = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
	/* WMAE get L62801/L62813 (EC EACS/ETCS), set L63638/L63654
	 * (WSMI 7/8, 9/0xA); capdata00 (WQA9) flags both 0x07.
	 */
	.has_instant_boot = true,
};

// Legion Pro 5 16ADR10 (83LT) - 2025, AMD + RTX 50
// BIOS: RLCN31WW, EC 0x5508. Facts below are from this unit's DSDT
// (L = dsdt.dsl line, issue #445); the WMA* methods live in \_SB.GZFD:
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L29032): get returns the 0x58-byte wmi_fan_table_read buffer, live
//   EC fields F9F0..F9F9 unless ODV1 == 4 (extreme), which returns the
//   static 1..10 placeholder; set copies the ten speeds (bytes
//   0x06..0x18 at even offsets, FSS0..FSS9) to F9F0..F9F9 and commits
//   via LECR(0xD0,1,1,2), no range check, mode/FSID bytes ignored.
//   Identical semantics to Q7CN, so FAN_SPEED_UNIT_LEVEL (see
//   wmi_read_fancurve_custom()) and only the speed attributes are
//   exposed (wmi_fancurve_speed_only). LENOVO_FAN_TABLE_DATA (WQA3,
//   L14777) maps level 1..10 to 1700..5100 RPM on fan 1, so
//   fan1_level_rpm_table/fan2_level_rpm_table appear automatically.
// - Other Method WMAE Get(0x11)/Set(0x12) (L29136) implements the
//   standard feature IDs: fan RPM 0x04030001/2 (FANS*0x64), CPU/GPU
//   temp 0x05040000/0x05050000, full speed 0x04020000 -> EC FNST (both
//   directions, mutex-protected), PL/OC 0x0101..0x0107/0x0201..0x0204
//   stored raw in the EC. 0x05010000 is the CPU socket temp (CPUS),
//   not a labeled IC sensor -> skip_ic_temp. PL/OC attributes stay
//   hidden (skip_oc_controls) until validated; only
//   cpu_temperature_limit, cpu_l1_tau and gpu_power_target_offset are
//   visible, as on Q7CN.
// - Power mode: GameZone WMAA SmartFanMode set 0x2C / get 0x2D
//   (L28308): quiet/balanced/performance, 0xFF custom, 0xE0 extreme;
//   performance/custom are parked on battery (ACTY check).
// - CPU Method WMAC only handles 0x0E (AMD thermal thresholds via
//   WECM, L29109), which the driver never calls.
// - Lights: KBBACKLIGHT WMAF get(1)/set(2) (L30748) drives the
//   keyboard (LECR 0xDA) and the Y-logo (LCST: 1 = on, 2 = off,
//   matching the driver's Gen 10 handling); light ID 0x05 (IO-port) is
//   unimplemented and reads 0, so it is skipped at runtime (-ENODEV).
// - EC0 at \_SB.PCI0.LPC0.EC0 (PNP0C09, GPE 7, IO 0x62/0x66, _STA
//   0x0F, L6910); EC RAM window ERAX @0xFEEC2400 len 0xFF (L7144, used
//   only for the read-only debugfs ecmemoryram dump); VPC0 GBMD/SBMC
//   (L9341/L9558) present. EC register offsets are not trusted on the
//   0x5508 generation (issue #491) - everything else goes through WMI.
// fan_fullspeed needs custom power mode (WMI3 FNST, cf. model_q7cn):
// on this unit a full-speed write through the wrong WMI method (GKCN
// fallback config) wedged the fans at ~18000 RPM until reboot, so the
// write is gated and must be validated with care (set in custom mode,
// then clear; reboot if it does not release).
static const struct model_config model_rlcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFEEC2400,
	.ramio_size = 0xFF,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
};

// Legion 5 15AHP10 (83M0) - 2025, AMD (Ryzen 7 260) + RTX 5060
// BIOS: RGCN27WW/RGCN35WW/RGCN36WW (issue #373), EC 0x5508. Facts below
// are from the RGCN35WW DSDT (L = dsdt.dsl line, attachments in issue
// #373); the WMA* methods live in \_SB.GZFD:
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L23364): get returns the 0x58-byte wmi_fan_table_read buffer, live
//   EC fields F9F0..F9F9 unless ODV1 == 4 (extreme), which returns the
//   static 1..10 placeholder; set copies the ten speeds (bytes
//   0x06..0x18 at even offsets, FSS0..FSS9) to F9F0..F9F9 and commits
//   via LECR(0xD0,1,1,2), no range check, mode/FSID bytes ignored.
//   Identical semantics to RLCN (83LT), so FAN_SPEED_UNIT_LEVEL (see
//   wmi_read_fancurve_custom()) and only the speed attributes are
//   exposed (wmi_fancurve_speed_only). LENOVO_FAN_TABLE_DATA (WQA3,
//   L17330) maps level 1..10 to 1800..4600 RPM on fan 1 (sensor 0x04)
//   and fan 2 (sensor 0x05), so fan1_level_rpm_table/
//   fan2_level_rpm_table appear automatically.
// - Other Method WMAE Get(0x11)/Set(0x12) (L23468) implements the
//   standard feature IDs: fan RPM 0x04030001/2 (FANS*0x64), CPU/GPU
//   temp 0x05040000/0x05050000, full speed 0x04020000 -> EC FNST (both
//   directions, mutex-protected), PL/OC 0x0101..0x0108/0x0201..0x0204
//   stored raw in the EC. 0x05010000 is the CPU socket temp (CPUS),
//   not a labeled IC sensor -> skip_ic_temp. PL/OC attributes stay
//   hidden (skip_oc_controls) until validated; only
//   cpu_temperature_limit, cpu_l1_tau and gpu_power_target_offset are
//   visible, as on Q7CN/RLCN.
// - Power mode: GameZone WMAA SmartFanMode set 0x2C / get 0x2D
//   (L22637): quiet/balanced/performance, 0xFF custom, 0xE0 extreme,
//   mapped from EC ASMC and ODV1 (ODV1 5..9 respect ACTY parking).
// - CPU Method WMAC only handles 0x0E (AMD thermal thresholds via
//   WECM, L23441), which the driver never calls.
// - Lights: KBBACKLIGHT WMAF get(1)/set(2) (L24902) drives the
//   keyboard (LECR 0xDA); the reporters' units have no lid/logo and no
//   IO-port lights (issue #373 report), so both light attributes are
//   skipped.
// - EC0 at \_SB.PCI0.LPC0.EC0 (L9987, _STA L9988, GPE 7, IO 0x62/0x66);
//   EC RAM window ERAX @0xFEEC2400 len 0xFF (L10205, used only for the
//   read-only debugfs ecmemoryram dump); VPC0 (VPC2004) GBMD/SBMC
//   (L12409/L12626) present. EC register offsets are not trusted on
//   the 0x5508 generation (issue #491) - everything else goes through
//   WMI.
// fan_fullspeed needs custom power mode (WMI3 FNST, cf. model_q7cn):
// on the 83LT a full-speed write through the wrong WMI method wedged
// the fans at maximum speed until reboot (model_rlcn), so the write is
// gated and must be validated with care (set in custom mode, then
// clear; reboot if it does not release).
static const struct model_config model_rgcn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.skip_ylogo_light = true,
	.skip_ioport_light = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFEEC2400,
	.ramio_size = 0xFF,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PCI0.LPC0.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PCI0.LPC0.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PCI0.LPC0.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
};

// Legion Y7000P IRX10 (83NN) - 2025, Intel Arrow Lake-HX + RTX 50
// BIOS: S9CN19WW (issue #506), EC 0x5508 (fw 2b0, read on the unit).
// Facts below are from the S9CN19WW DSDT (L = dsdt.dsl line,
// attachment in issue #506); the WMA* methods live in \_SB.GZFD:
// - Fan Method WMAB implements only Fan_Get_Table(5)/Fan_Set_Table(6)
//   (L41247): get returns the 0x58-byte LFGT buffer with the live EC
//   fields F9F0..F9F9 (static 1..10 placeholder when ODV1 == 4, i.e.
//   extreme mode); set copies the ten speeds (bytes 0x06..0x18 at even
//   offsets) to F9F0..F9F9 and commits via LECR(0xD0,1,1,2), no range
//   check, mode/FSID bytes ignored. The bytes are fan LEVELS 1..10
//   (LENOVO_FAN_TABLE_DATA, WQA3 L35223, maps level 1..10 to
//   1700..5100 RPM on fan 1), one table for all fans, temperature axis
//   fixed by the EC, hence FAN_SPEED_UNIT_LEVEL and only the speed
//   attributes are exposed (wmi_fancurve_speed_only).
// - Other Method WMAE Get(0x11)/Set(0x12) (L41328) implements the
//   standard feature ids: fan RPM 0x04030001/2 (FANS/FA2S*0x64),
//   CPU/GPU temp 0x05040000/0x05050000 (CPUT/GPUT), full speed
//   0x04020000 -> EC FNST (both directions), PL/OC 0x0101..0x0107/
//   0x0201..0x0204 stored raw in the EC. 0x05010000 is the CPU socket
//   temp (CPUS), not a labeled IC sensor -> skip_ic_temp. PL/OC
//   attributes stay hidden (skip_oc_controls) until validated; only
//   cpu_temperature_limit, cpu_l1_tau and gpu_power_target_offset are
//   visible, as on Q7CN.
// - Power mode: GameZone WMAA SmartFanMode set 0x2C / get 0x2D
//   (L40638/L40509): quiet/balanced/performance, 0xFF custom, 0xE0
//   extreme.
// - CPU Method WMAC is an empty stub (L41324), so power limits go
//   through WMAE (WMI3), as on model_q7cn/model_rlcn.
// - Lights: KBBACKLIGHT WMAF get(1)/set(2) (L42947) drives the
//   keyboard (id 0x00, LECR 0xDA levels 1..3) and the Y-logo (id 0x03,
//   LCST on/off via LECR 0xDA); light id 0x05 (IO-port) is
//   unimplemented and reads 0, so it is skipped at runtime (-ENODEV).
// - EC0 at \_SB.PC00.LPCB.EC0 (L28222); EC RAM window ERAX @0xFE0B0400
//   (L28492, len 0xFF; F9FT/ECB2 at +0x100/+0x200, ramio_size 0x300)
//   is used only by the read-only debugfs ecmemoryram dump; EC
//   register offsets are not trusted on the 0x5508 generation (issue
//   #491) - everything else goes through WMI. VPC0 _STA/_CFG
//   (L30714/L30719), GBMD/SBMC (L30881/L31098) present.
// Runtime validation on the reporters' units (issue #506, huverse and
// AXFOX, with the WMI-only config from this family): WMI powermode and
// platform profile work; WMI3 CPU temp and fan RPM are correct (idle
// ~48 C/fans off, load ~85-88 C/~2000-2500 RPM) while raw EC reads are
// garbage (~18045/~16740 RPM), matching the EC-misalignment quirk of
// this generation; keyboard backlight and Y-logo LED drivers init
// (the platform::kbd_backlight name collision with ideapad_laptop is
// cosmetic and handled by the rename).
// fan_fullspeed is gated behind custom power mode like model_q7cn/
// model_rlcn: on this generation a full-speed write through the wrong
// path can wedge the fans at maximum speed until reboot (model_rlcn).
static const struct model_config model_s9cn = {
	.registers = &ec_register_offsets_v0,
	.check_embedded_controller_id = true,
	.embedded_controller_id = 0x5508,
	.memoryio_physical_ec_start = 0xC400,
	.memoryio_size = 0x300,
	.has_minifancurve = false,
	.has_custom_powermode = true,
	.has_extreme_powermode = true,
	.access_method_powermode = ACCESS_METHOD_WMI,
	.access_method_keyboard = ACCESS_METHOD_WMI2,
	.access_method_temperature = ACCESS_METHOD_WMI3,
	.access_method_fanspeed = ACCESS_METHOD_WMI3,
	.access_method_fancurve = ACCESS_METHOD_WMI3,
	.access_method_fanfullspeed = ACCESS_METHOD_WMI3,
	.access_method_powerlimits = ACCESS_METHOD_WMI3,
	.fanfullspeed_requires_custom_powermode = true,
	.skip_ic_temp = true,
	.skip_oc_controls = true,
	.skip_lockfancontroller = true,
	/* Fan Method WMAB implements only ids 5/6 (see the header comment). */
	.skip_fan_maxspeed = true,
	.acpi_check_dev = false,
	.ramio_physical_start = 0xFE0B0400,
	.ramio_size = 0x300,
	.acpi_paths = { [ACPI_PATH_STA] = "\\_SB.PC00.LPCB.EC0.VPC0._STA",
			[ACPI_PATH_CFG] = "\\_SB.PC00.LPCB.EC0.VPC0._CFG",
			[ACPI_PATH_READ_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.GBMD",
			[ACPI_PATH_WRITE_RAPIDCHARGE] =
				"\\_SB.PC00.LPCB.EC0.VPC0.SBMC" },
	.has_fancurve_defaults = true,
	.wmi_fancurve_speed_only = true,
	.has_fan_unlock = false,
	.has_fn_lock = false,
	.has_flip_to_start = true,
};

static const struct dmi_system_id denylist[] = { {} };

static const struct dmi_system_id optimistic_allowlist[] = {
	{
		// Release year: 2021
		// Generation: 6
		// Name: Legion 5, Legion 5 pro, Legion 7
		// Family: Legion 5 15ACH6H, ...
		.ident = "GKCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "GKCN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		.ident = "Legion R7000P APH8 (82Y9) - EC 0x8227",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "82Y9"),
			DMI_MATCH(DMI_BIOS_VERSION, "M3CN"),
		},
		.driver_data = (void *)&model_m3cn_8227
	},
	{
		// Lenovo LOQ 15APH8 (82XT), BIOS LYCN (issue #602); same AMD
		// Phoenix + EC 0x8227 platform as the Legion R7000P APH8
		// (82Y9, M3CN) above. The reporter's debugfs dump shows EC
		// chip 0x8227, correct WMI3 temperatures/fan RPM and zeroed
		// EC-direct reads, matching model_m3cn_8227. Product-qualified
		// in case the LYCN BIOS line is shared by other chassis.
		.ident = "LOQ 15APH8 (82XT) - EC 0x8227",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "82XT"),
			DMI_MATCH(DMI_BIOS_VERSION, "LYCN"),
		},
		.driver_data = (void *)&model_m3cn_8227
	},
	{
		// Release year: 2020
		.ident = "EUCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "EUCN"),
		},
		.driver_data = (void *)&model_eucn
	},
	{
		// Release year: 2020
		.ident = "EFCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "EFCN"),
		},
		.driver_data = (void *)&model_efcn
	},
	{
		// Release year: 2020
		.ident = "FSCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "FSCN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		// Release year: 2021
		.ident = "HHCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "HHCN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		// Release year: 2022
		.ident = "H1CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "H1CN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		// Release year: 2022
		.ident = "JUCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "JUCN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		// Release year: 2022
		.ident = "KFCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "KFCN"),
		},
		.driver_data = (void *)&model_kfcn
	},
	{
		// Release year: 2021
		.ident = "HACN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "HACN"),
		},
		.driver_data = (void *)&model_hacn
	},
	{
		// Release year: 2021
		.ident = "G9CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "G9CN"),
		},
		.driver_data = (void *)&model_v0
	},
	{
		// Release year: 2022
		.ident = "K9CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "K9CN"),
		},
		.driver_data = (void *)&model_k9cn
	},
	{
		// e.g. IdeaPad Gaming 3 15ARH05
		.ident = "FCCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "FCCN"),
		},
		.driver_data = (void *)&model_fccn
	},
	{
		// e.g. IdeaPad Gaming 3 15ARH05 (8K21)
		.ident = "H4CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "H4CN"),
		},
		.driver_data = (void *)&model_h3cn
	},
	{
		// e.g. Ideapad Gaming 3 15ACH6
		.ident = "H3CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "H3CN"),
		},
		.driver_data = (void *)&model_h3cn
	},
	{
		// e.g. IdeaPad Gaming 3 15ARH7 (2022)
		.ident = "JNCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "JNCN"),
		},
		.driver_data = (void *)&model_jncn
	},
	{
		// 2020, seems very different in ACPI dissassembly
		.ident = "E9CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "E9CN"),
		},
		.driver_data = (void *)&model_e9cn
	},
	{
		// e.g. Legion Y7000 (older version)
		.ident = "8JCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "8JCN"),
		},
		.driver_data = (void *)&model_8jcn
	},
	{
		// e.g. Legion 7i Pro 2023
		.ident = "KWCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "KWCN"),
		},
		.driver_data = (void *)&model_kwcn
	},
	{
		// e.g. Legion 5 15IMH6
		.ident = "G8CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "G8CN"),
		},
		.driver_data = (void *)&model_g8cn
	},
	{
		// e.g. Legion Pro 5 2023 or R9000P
		.ident = "LPCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "LPCN"),
		},
		.driver_data = (void *)&model_lpcn
	},
	{
		// e.g. Lenovo Legion 5i/Y7000 2019 PG0
		.ident = "BHCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "BHCN"),
		},
		.driver_data = (void *)&model_bhcn
	},
	{
		// e.g. Lenovo Legion 7 16IAX7 (82TD) (BIOS K1CN48WW)
		.ident = "K1CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "K1CN"),
		},
		.driver_data = (void *)&model_k1cn
	},
	{
		// Legion 7 16IRX9 (83FD), BIOS NSCN (issue #617): WMAB 5/6
		// level fan table like model_t2cn. Must precede the generic
		// NSCN entry below.
		.ident = "NSCN 83FD",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83FD"),
			DMI_MATCH(DMI_BIOS_VERSION, "NSCN"),
		},
		.driver_data = (void *)&model_nscn_83fd
	},
	{
		// e.g. Lenovo 7 16IAX9
		.ident = "NSCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NSCN"),
		},
		.driver_data = (void *)&model_nscn
	},
	{
		// e.g. Legion 9 18IAX10 (83EY)
		.ident = "RZCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "RZCN"),
		},
		.driver_data = (void *)&model_rzcn
	},
	{
		// e.g. Legion Y720
		.ident = "4GCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "4GCN"),
		},
		.driver_data = (void *)&model_4gcn
	},
	{
		// e.g. Legion Slim 5 16APH8 2023
		.ident = "M3CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "M3CN"),
		},
		.driver_data = (void *)&model_m3cn
	},
	{
		// e.g. Legion Y7000p-1060
		.ident = "9VCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "9VCN"),
		},
		.driver_data = (void *)&model_9vcn
	},
	{
		// e.g. Legion Y9000X
		.ident = "JYCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "JYCN"),
		},
		.driver_data = (void *)&model_v2022
	},
	{
		// e.g. Legion Y740-15IRH, older model e.g. with GTX 1660
		.ident = "BVCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "BVCN"),
		},
		.driver_data = (void *)&model_bvcn
	},
	{
		// e.g. Legion 5 Pro 16IAH7H with a RTX 3070 Ti
		.ident = "J2CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "J2CN"),
		},
		.driver_data = (void *)&model_j2cn
	},
	{
		// e.g. Lenovo Yoga 7 16IAH7 with GPU Intel DG2 Arc A370M
		.ident = "J1CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "J1CN"),
		},
		.driver_data = (void *)&model_j1cn
	},
	{
		// e.g. Legion Slim 7 16IRH8 (2023) with RTX 4070
		.ident = "M0CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "M0CN"),
		},
		.driver_data = (void *)&model_m0cn
	},
	{
		// e.g. Legion Slim 7 16IRH8 (2023) AMD Ryzen 7 7840HS with RTX 4060
		.ident = "M1CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "M1CN"),
		},
		.driver_data = (void *)&model_m1cn
	},
	{
		// e.g. Legion Slim 5 16IRH8 (2023) with RTX 4070
		.ident = "M2CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "M2CN"),
		},
		.driver_data = (void *)&model_m2cn
	},
	{
		// e.g. Lenovo Yoga Slim 7 gen 8 (2023)
		.ident = "M6CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "M6CN"),
		},
		.driver_data = (void *)&model_m6cn
	},
	{
		// e.g. Yoga Slim 7-14ARE05
		.ident = "DMCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "DMCN"),
		},
		.driver_data = (void *)&model_dmcn
	},
	{
		// e.g. Yoga Slim 7 Pro 14ARH7
		.ident = "KHCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "KHCN"),
		},
		.driver_data = (void *)&model_khcn
	},
	{
		// e.g. Yoga Slim 7 ProX 14ARH7
		.ident = "JVCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "JVCN"),
		},
		.driver_data = (void *)&model_khcn
	},
	{
		// e.g. LOQ 15IRH8
		.ident = "LZCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "LZCN"),
		},
		.driver_data = (void *)&model_lzcn
	},
	{
		// e.g. LOQ 15IRX9
		.ident = "NECN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NECN"),
		},
		.driver_data = (void *)&model_necn
	},
	{
		// e.g. LOQ 15AHP9 (AMD Ryzen 7 8845HS + RTX 4060)
		.ident = "NZCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NZCN"),
		},
		.driver_data = (void *)&model_nzcn
	},
	{
		// e.g. LOQ 15AHP10 (AMD Ryzen 7 250 + RTX 5050)
		.ident = "R8CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "R8CN"),
		},
		.driver_data = (void *)&model_r8cn
	},
	{
		// e.g. Legion 5 15AHP11 (83Q7, AMD Ryzen 7 250 + RTX 5050) - same
		// platform as LOQ 15AHP10; EC 0x5509, fancurve via WMI3 (#504)
		.ident = "T2CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "T2CN"),
		},
		.driver_data = (void *)&model_t2cn
	},
	{
		// e.g. Legion 5 15AGP11 (83Q6, AMD Ryzen AI 9 465 + RTX 5070) -
		// same platform as the 83Q7 above; EC 0x5509, fancurve via
		// WMI3 (#504)
		.ident = "T3CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "T3CN"),
		},
		.driver_data = (void *)&model_t2cn
	},
	{
		// e.g. Legion 5 15IRX10 (83LY)
		.ident = "QNCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "QNCN"),
		},
		.driver_data = (void *)&model_qncn
	},
	{
		//e.g. LOQ 15ARP9 (83JC, AMD Ryzen 7 7435HS + RTX 4050)
		.ident = "PQCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "PQCN"),
		},
		.driver_data = (void *)&model_nzcn
	},
	{
		// e.g. Legion Pro 5 16IRX9 (83DF)
		.ident = "N0CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "N0CN"),
		},
		.driver_data = (void *)&model_g8cn
	},
	{
		// Legion Pro 7 16IRX9H (83DE)
		.ident = "N2CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83DE"),
			DMI_MATCH(DMI_BIOS_VERSION, "N2CN"),
		},
		.driver_data = (void *)&model_n2cn
	},
	{
		// e.g. Legion 9 16IRX9 (83G0)
		.ident = "NXCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NXCN"),
		},
		.driver_data = (void *)&model_nxcn
	},
	{
		// e.g. Legion Slim 5 16AHP9 (2024) - Model 83DH
		// AMD Ryzen 7 8845HS with RTX 4060/4070
		.ident = "NRCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NRCN"),
		},
		.driver_data = (void *)&model_nrcn
	},
	{
		// LOQ 15IRX10 (83JE, Intel + RTX 50), BIOS R3CN;
		// EC3 RPM/temperature curve verified in custom mode (#535)
		.ident = "R3CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "R3CN"),
		},
		.driver_data = (void *)&model_r3cn
	},
	{
		// Legion 7 16IAX10 (83KY) - Intel Core Ultra 7 255HX + RTX 5060
		.ident = "RXCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "RXCN"),
		},
		.driver_data = (void *)&model_rxcn
	},
	{
		// Legion 5 16IAX10 (83NX, Intel Core Ultra 9 275HX + RTX 5060 Max-Q)
		// EC chip id 0x5508, WMI3 fan/temp/power-limit reads confirmed on
		// the unit; see model_q6cn comment. Product-qualified because the
		// Q6CN BIOS is also shared by the 83LU Legion Pro 5 16IAX10H
		// (model_q6cn_lu below, issue #337) and the 83F3 sibling, which are
		// different chassis (different ramio window, no minifancurve, etc.)
		.ident = "Q6CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83NX"),
			DMI_MATCH(DMI_BIOS_VERSION, "Q6CN"),
		},
		.driver_data = (void *)&model_q6cn
	},
	{
		// Legion Pro 5 16IAX10H (83LU, Intel Core Ultra 9 275HX +
		// RTX 5070 Ti), BIOS Q6CN (issue #337); DSDT-validated sibling of
		// the 83F5 Legion Pro 7 16IAX10H below (same ERAX window and
		// WMAA/WMAB/WMAE/WMAF layout), distinct from the 83NX Legion 5
		// entry above
		.ident = "Q6CN 83LU",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83LU"),
			DMI_MATCH(DMI_BIOS_VERSION, "Q6CN"),
		},
		.driver_data = (void *)&model_q6cn_lu
	},
	{
		// Legion Pro 5 16IAX10 (83F3), BIOS Q6CN (issue #471); the
		// Legion Pro 5 sibling of the 83LU above - same Q6CN BIOS line
		// and ERAX window, own chassis validation. Product-qualified
		// because the Q6CN BIOS is also shared with the 83NX Legion 5.
		.ident = "Q6CN 83F3",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83F3"),
			DMI_MATCH(DMI_BIOS_VERSION, "Q6CN"),
		},
		.driver_data = (void *)&model_q6cn_f3
	},
	{
		// Legion Pro 7 16IAX10H (83F5), BIOS Q7CN; product-qualified
		// because the Q6CN/83LU and 83F3 siblings are different chassis
		.ident = "Q7CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83F5"),
			DMI_MATCH(DMI_BIOS_VERSION, "Q7CN"),
		},
		.driver_data = (void *)&model_q7cn
	},
	{
		// Legion Pro 5 16ADR10 (83LT), BIOS RLCN (issue #445); the
		// 83LV R9000P 2025 shares the RLCN BIOS line but is a different
		// chassis, so match the product name and add it only after its
		// own validation
		.ident = "RLCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83LT"),
			DMI_MATCH(DMI_BIOS_VERSION, "RLCN"),
		},
		.driver_data = (void *)&model_rlcn
	},
	{
		// Legion 5 15AHP10 (83M0), BIOS RGCN (issue #373); AMD +
		// RTX 50, EC 0x5508, DSDT-validated sibling of the 83LT
		// (RLCN) above: same ERAX window and WMAA/WMAB/WMAE/WMAF
		// layout. Product-qualified in case the RGCN BIOS line is
		// shared by other chassis.
		.ident = "RGCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83M0"),
			DMI_MATCH(DMI_BIOS_VERSION, "RGCN"),
		},
		.driver_data = (void *)&model_rgcn
	},
	{
		// Legion 5 15IAX10 (83F0)
		.ident = "S2CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "S2CN"),
		},
		.driver_data = (void *)&model_s2cn
	},
	{
		// LOQ Essential 15IRX11 (83SC), BIOS SECN, EC 0x5508
		.ident = "SECN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "SECN"),
		},
		.driver_data = (void *)&model_secn
	},
	{
		// Legion Pro 5 16AFR10 (83F2), BIOS RECN, EC 0x5508; fancurve
		// via WMI3 (#527)
		.ident = "RECN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "RECN"),
		},
		.driver_data = (void *)&model_recn
	},
	{
		// Legion Y7000P IRX10 (83NN), BIOS S9CN (issue #506);
		// DSDT-validated WMI-only sibling of the 83F5 Legion Pro 7
		// 16IAX10H (model_q7cn), EC 0x5508. Product-qualified in
		// case the S9CN BIOS line is shared by other chassis.
		.ident = "S9CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83NN"),
			DMI_MATCH(DMI_BIOS_VERSION, "S9CN"),
		},
		.driver_data = (void *)&model_s9cn
	},
	{
		// e.g. Legion 5 16IRX9 (83DG)
		.ident = "NMCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "NMCN"),
		},
		.driver_data = (void *)&model_nmcn
	},
	{
		// e.g. LOQ 15IAX9E
		.ident = "Q8CN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "Q8CN"),
		},
		.driver_data = (void *)&model_q8cn
	},
	{
		// e.g. Legion 5i Gen 10 (83VK)
		.ident = "UFCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "UFCN"),
		},
		.driver_data = (void *)&model_ufcn
	},
	{
		// e.g. Legion Y7000 IRX9 (83JJ)
		.ident = "PTCN",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
			DMI_MATCH(DMI_BIOS_VERSION, "PTCN"),
		},
		.driver_data = (void *)&model_nmcn
	},
	{}
};

/* ================================= */
/* ACPI and WMI access               */
/* ================================= */
//global,
//wanted to use _priv->conf but involves
//move all structs/defn from all the way down up
static const struct model_config *_model;

static const char *get_model_acpi_path(const struct model_config *model,
				       enum acpi_paths_inventory_ids id)
{
	if (id < 0 || id >= ACPI_PATH_MAX)
		return NULL;
	if (model->acpi_paths[id] != NULL)
		return model->acpi_paths[id];
	return default_acpi_paths[id];
}

// function from ideapad-laptop.c
static int eval_int(struct acpi_device *adev, const char *name,
		    unsigned long *res)
{
	unsigned long long result;
	acpi_status status;
	acpi_handle handle;

	if (!adev) {
		/* No ACPI device to evaluate relative to: resolve the
		 * fully-qualified name from the ACPI root.
		 */
		status = acpi_get_handle(NULL, (char *)name, &handle);
		if (ACPI_FAILURE(status))
			return -ENODEV;
	} else {
		handle = adev->handle;
	}
	status = acpi_evaluate_integer(handle, (char *)name, NULL, &result);
	if (ACPI_FAILURE(status))
		return -EIO;

	*res = result;

	return 0;
}

// function from ideapad-laptop.c
static int exec_simple_method(struct acpi_device *adev, const char *name,
			      unsigned long arg)
{
	acpi_handle handle;
	acpi_status status;

	if (!adev) {
		/* No ACPI device to evaluate relative to: resolve the
		 * fully-qualified name from the ACPI root.
		 */
		status = acpi_get_handle(NULL, (char *)name, &handle);
		if (ACPI_FAILURE(status))
			return -ENODEV;
	} else {
		handle = adev->handle;
	}
	status = acpi_execute_simple_method(handle, (char *)name, arg);

	return ACPI_FAILURE(status) ? -EIO : 0;
}

static bool acpi_method_exists(struct acpi_device *adev, const char *name)
{
	acpi_handle handle;
	acpi_status status;

	if (!name)
		return false;
	status = acpi_get_handle(adev ? adev->handle : NULL, (char *)name,
				 &handle);

	return ACPI_SUCCESS(status);
}

// function from ideapad-laptop.c
static int exec_sbmc(struct acpi_device *adev, unsigned long arg)
{
	// \_SB.PCI0.LPC0.EC0.VPC0.SBMC
	const char *acpi_path;

	acpi_path = get_model_acpi_path(_model, ACPI_PATH_WRITE_RAPIDCHARGE);
	return exec_simple_method(adev, acpi_path, arg);
}

static int exec_sals(struct acpi_device *adev, unsigned long arg)
{
	const char *acpi_path;

	acpi_path = get_model_acpi_path(_model, ACPI_PATH_WRITE_FNLOCK);
	if (!acpi_path)
		return -EINVAL;
	return exec_simple_method(adev, acpi_path, arg);
}

//static int eval_qcho(acpi_handle handle, unsigned long *res)
//{
//	// \_SB.PCI0.LPC0.EC0.QCHO
//	return eval_int(handle, "QCHO", res);
//}

static int eval_gbmd(struct acpi_device *adev, unsigned long *res)
{
	const char *acpi_path;

	acpi_path = get_model_acpi_path(_model, ACPI_PATH_READ_RAPIDCHARGE);
	return eval_int(adev, acpi_path, res);
}

static int eval_hals(struct acpi_device *adev, unsigned long *res)
{
	const char *path;

	path = get_model_acpi_path(_model, ACPI_PATH_READ_FNLOCK);
	if (!path)
		return -EINVAL;
	return eval_int(adev, path, res);
}

static int eval_spmo(struct acpi_device *adev, unsigned long *res)
{
	// \_SB.PCI0.LPC0.EC0.QCHO
	const char *acpi_path;

	acpi_path = get_model_acpi_path(_model, ACPI_PATH_READ_POWERMODE);
	return eval_int(adev, acpi_path, res);
}

static int acpi_process_buffer_to_ints(const char *id_name, int id_nr,
				       acpi_status status,
				       struct acpi_buffer *out_buffer, u8 *res,
				       size_t ressize)
{
	// seto to NULL call kfree on NULL if next function call fails
	union acpi_object *out = NULL;
	size_t i;
	int error = 0;

	if (ACPI_FAILURE(status)) {
		pr_info("ACPI evaluation error for: %s:%d\n", id_name, id_nr);
		error = -EFAULT;
		goto err;
	}

	out = out_buffer->pointer;
	if (!out) {
		pr_info("Unexpected ACPI result for %s:%d\n", id_name, id_nr);
		error = -AE_ERROR;
		goto err;
	}

	if (out->type != ACPI_TYPE_BUFFER || out->buffer.length != ressize) {
		pr_info("Unexpected ACPI result for %s:%d: expected type %d but got %d; expected length %lu but got %u;\n",
			id_name, id_nr, ACPI_TYPE_BUFFER, out->type, ressize,
			out->buffer.length);
		error = -AE_ERROR;
		goto err;
	}

	// Reduced verbosity (only printing when ACPI result have bad parameters)
	//	pr_info("ACPI result for %s:%d: ACPI buffer length: %u\n", id_name,
	//		id_nr, out->buffer.length);

	for (i = 0; i < ressize; ++i)
		res[i] = out->buffer.pointer[i];
	error = 0;

err:
	kfree(out);
	return error;
}

//static int exec_ints(acpi_handle handle, const char *method_name,
//		     struct acpi_object_list *params, u8 *res, size_t ressize)
//{
//	acpi_status status;
//	struct acpi_buffer out_buffer = { ACPI_ALLOCATE_BUFFER, NULL };

//	status = acpi_evaluate_object(handle, (acpi_string)method_name, params,
//				      &out_buffer);

//	return acpi_process_buffer_to_ints(method_name, 0, status, &out_buffer,
//					   res, ressize);
//}

static int wmi_exec_ints(const char *guid, u8 instance, u32 method_id,
			 const struct acpi_buffer *params, u8 *res,
			 size_t ressize)
{
	acpi_status status;
	struct acpi_buffer out_buffer = { ACPI_ALLOCATE_BUFFER, NULL };

	if (!wmi_has_guid(guid))
		return -ENODEV;

	status = wmi_evaluate_method(guid, instance, method_id, params,
				     &out_buffer);
	return acpi_process_buffer_to_ints(guid, method_id, status, &out_buffer,
					   res, ressize);
}

static int wmi_exec_int(const char *guid, u8 instance, u32 method_id,
			const struct acpi_buffer *params, unsigned long *res)
{
	acpi_status status;
	struct acpi_buffer out_buffer = { ACPI_ALLOCATE_BUFFER, NULL };
	// set to NULL and call kfree on NULL if next function call fails
	union acpi_object *out = NULL;
	int error = 0;

	if (!wmi_has_guid(guid))
		return -ENODEV;

	status = wmi_evaluate_method(guid, instance, method_id, params,
				     &out_buffer);

	if (ACPI_FAILURE(status)) {
		pr_info("WMI evaluation error for: %s:%d\n", guid, method_id);
		error = -EFAULT;
		goto err;
	}

	out = out_buffer.pointer;
	if (!out) {
		pr_info("Unexpected ACPI result for %s:%d", guid, method_id);
		error = -AE_ERROR;
		goto err;
	}

	if (out->type != ACPI_TYPE_INTEGER) {
		pr_info("Unexpected ACPI result for %s:%d: expected type %d but got %d\n",
			guid, method_id, ACPI_TYPE_INTEGER, out->type);
		error = -AE_ERROR;
		goto err;
	}

	*res = out->integer.value;
	error = 0;

err:
	kfree(out);
	return error;
}

static int wmi_exec_noarg_int(const char *guid, u8 instance, u32 method_id,
			      unsigned long *res)
{
	struct acpi_buffer params;

	params.length = 0;
	params.pointer = NULL;
	return wmi_exec_int(guid, instance, method_id, &params, res);
}

/* Query a WMI data block instance and copy the result into a struct of
 * exactly ressize bytes (as in PR #347). Fails when the block's buffer
 * has a different size, so variable-size rows are rejected up front.
 */
static int wmi_exec_query_ints(const char *guid, u8 instance, u8 *res,
			       size_t ressize)
{
	struct acpi_buffer out_buffer = { ACPI_ALLOCATE_BUFFER, NULL };
	acpi_status status;

	status = wmi_query_block(guid, instance, &out_buffer);

	return acpi_process_buffer_to_ints(guid, instance, status, &out_buffer,
					   res, ressize);
}

static int wmi_exec_noarg_int_or_buffer(const char *guid, u8 instance,
					u32 method_id, size_t ressize,
					size_t index, unsigned long *res)
{
	struct acpi_buffer params;
	struct acpi_buffer out_buffer = { ACPI_ALLOCATE_BUFFER, NULL };
	union acpi_object *out = NULL;
	acpi_status status;
	int error = 0;

	params.length = 0;
	params.pointer = NULL;
	if (!wmi_has_guid(guid))
		return -ENODEV;

	status = wmi_evaluate_method(guid, instance, method_id, &params,
				     &out_buffer);
	if (ACPI_FAILURE(status)) {
		pr_info("WMI evaluation error for: %s:%d\n", guid, method_id);
		error = -EIO;
		goto out;
	}

	out = out_buffer.pointer;
	if (!out) {
		pr_info("Unexpected WMI result for %s:%d\n", guid, method_id);
		error = -EIO;
		goto out;
	}

	switch (out->type) {
	case ACPI_TYPE_INTEGER:
		if (index) {
			pr_info("Unexpected WMI result for %s:%d: integer value %llu cannot satisfy byte index %zu\n",
				guid, method_id, out->integer.value, index);
			error = -EINVAL;
			break;
		}
		*res = out->integer.value;
		break;
	case ACPI_TYPE_BUFFER:
		if (out->buffer.length != ressize) {
			pr_info("Unexpected WMI result for %s:%d: expected buffer length %zu but got %u\n",
				guid, method_id, ressize, out->buffer.length);
			if (out->buffer.length)
				print_hex_dump(
					KERN_INFO,
					"WMI result: ", DUMP_PREFIX_OFFSET, 16,
					1, out->buffer.pointer,
					min_t(u32, out->buffer.length, 64),
					false);
			error = -EINVAL;
			break;
		}
		*res = out->buffer.pointer[index];
		break;
	case ACPI_TYPE_PACKAGE:
		pr_info("Unexpected WMI result for %s:%d: type %u, package size %u\n",
			guid, method_id, out->type, out->package.count);
		error = -EINVAL;
		break;
	default:
		pr_info("Unexpected WMI result for %s:%d: type %u\n", guid,
			method_id, out->type);
		error = -EINVAL;
		break;
	}

out:
	kfree(out);
	return error;
}

static int wmi_exec_arg(const char *guid, u8 instance, u32 method_id, void *arg,
			size_t arg_size)
{
	struct acpi_buffer params;
	acpi_status status;

	params.length = arg_size;
	params.pointer = arg;
	if (!wmi_has_guid(guid))
		return -ENODEV;

	status = wmi_evaluate_method(guid, instance, method_id, &params, NULL);

	if (ACPI_FAILURE(status))
		return -EIO;
	return 0;
}

/* ================================= */
/* Lenovo WMI config                 */
/* ================================= */
#define LEGION_WMI_GAMEZONE_GUID "887B54E3-DDDC-4B2C-8B88-68A26A8835D0"
// GPU over clock
#define WMI_METHOD_ID_ISSUPPORTGPUOC 4

// Fan speed
// only fully implemented for some models here
// often implemented in other classes and methods too
// below
#define WMI_METHOD_ID_GETFAN1SPEED 8
#define WMI_METHOD_ID_GETFAN2SPEED 9

// Version of ACPI
#define WMI_METHOD_ID_GETVERSION 11
// Does it support CPU overclock?
#define WMI_METHOD_ID_ISSUPPORTCPUOC 14
// Temperatures
// only fully implemented for some models here
// often implemented in other classes and methods too
// below
#define WMI_METHOD_ID_GETCPUTEMP 18
#define WMI_METHOD_ID_GETGPUTEMP 19

// two state keyboard light
#define WMI_METHOD_ID_GETKEYBOARDLIGHT 37
#define WMI_METHOD_ID_SETKEYBOARDLIGHT 36
// toggle win key
// 0 = win key enabled; 1 = win key disabled
#define WMI_METHOD_ID_ISSUPPORTDISABLEWINKEY 21
#define WMI_METHOD_ID_GETWINKEYSTATUS 23
#define WMI_METHOD_ID_SETWINKEYSTATUS 22
// toggle touchpad
//0 = touchpad enabled; 1 = touchpad disabled
#define WMI_METHOD_ID_ISSUPPORTDISABLETP 24
#define WMI_METHOD_ID_GETTPSTATUS 26
#define WMI_METHOD_ID_SETTPSTATUS 25
// GSync
#define WMI_METHOD_ID_ISSUPPORTGSYNC 40
#define WMI_METHOD_ID_GETGSYNCSTATUS 41
#define WMI_METHOD_ID_SETGSYNCSTATUS 42
//smartFanMode = powermode
#define WMI_METHOD_ID_ISSUPPORTSMARTFAN 43
#define WMI_METHOD_ID_GETSMARTFANMODE 45
#define WMI_METHOD_ID_SETSMARTFANMODE 44
// Fan ceiling unlock (discovered on KWCN54WW / Legion Pro 7 16IRX8H, May 2026).
// WMAA(0, 0x0D, arg) toggles NCMD(0x59, 0x77) on the EC's alt-namespace
// command port. arg=1 raises the firmware fan ceiling from the default
// Linux cap (~4400 RPM in Performance mode on this firmware) to the EC's
// high-end fan curve subtable (~7000-7100 RPM observed on i9-13900HX +
// RTX 4080 Laptop). arg=0 reverts. See johnfanv2/LenovoLegionLinux #429.
#define WMI_METHOD_ID_FAN_EXTREME_TOGGLE 13
// power charge mode
#define WMI_METHOD_ID_GETPOWERCHARGEMODE 47
// overdrive of display to reduce latency
// 0=off, 1=on
#define WMI_METHOD_ID_ISSUPPORTOD 49
#define WMI_METHOD_ID_GETODSTATUS 50
#define WMI_METHOD_ID_SETODSTATUS 51
// thermal mode = power mode used for cooling
#define WMI_METHOD_ID_GETTHERMALMODE 55
// get max frequency of core 0
#define WMI_METHOD_ID_GETCPUMAXFREQUENCY 60
// check if AC adapter has enough power to overclock
#define WMI_METHOD_ID_ISACFITFOROC 62
// set iGPU (GPU packaged with CPU) state
#define WMI_METHOD_ID_ISSUPPORTIGPUMODE 63
#define WMI_METHOD_ID_GETIGPUMODESTATUS 64
#define WMI_METHOD_ID_SETIGPUMODESTATUS 65
#define WMI_METHOD_ID_NOTIFYDGPUSTATUS 66
enum IGPUState {
	IGPUState_default = 0,
	IGPUState_iGPUOnly = 1,
	IGPUState_auto = 2
};

#define WMI_GUID_LENOVO_CPU_METHOD "14afd777-106f-4c9b-b334-d388dc7809be"
#define WMI_METHOD_ID_CPU_GET_SUPPORT_OC_STATUS 15
#define WMI_METHOD_ID_CPU_GET_OC_STATUS 1
#define WMI_METHOD_ID_CPU_SET_OC_STATUS 2

// ppt limit slow
#define WMI_METHOD_ID_CPU_GET_SHORTTERM_POWERLIMIT 3
#define WMI_METHOD_ID_CPU_SET_SHORTTERM_POWERLIMIT 4
// ppt stapm
#define WMI_METHOD_ID_CPU_GET_LONGTERM_POWERLIMIT 5
#define WMI_METHOD_ID_CPU_SET_LONGTERM_POWERLIMIT 6
// default power limit
#define WMI_METHOD_ID_CPU_GET_DEFAULT_POWERLIMIT 7
// peak power limit
#define WMI_METHOD_ID_CPU_GET_PEAK_POWERLIMIT 8
#define WMI_METHOD_ID_CPU_SET_PEAK_POWERLIMIT 9
// apu sppt powerlimit
#define WMI_METHOD_ID_CPU_GET_APU_SPPT_POWERLIMIT 12
#define WMI_METHOD_ID_CPU_SET_APU_SPPT_POWERLIMIT 13
// cross loading powerlimit
#define WMI_METHOD_ID_CPU_GET_CROSS_LOADING_POWERLIMIT 16
#define WMI_METHOD_ID_CPU_SET_CROSS_LOADING_POWERLIMIT 17

#define WMI_GUID_LENOVO_GPU_METHOD "da7547f1-824d-405f-be79-d9903e29ced7"
// overclock GPU possible
#define WMI_METHOD_ID_GPU_GET_OC_STATUS 1
#define WMI_METHOD_ID_GPU_SET_OC_STATUS 2
// dynamic boost power
#define WMI_METHOD_ID_GPU_GET_PPAB_POWERLIMIT 3
#define WMI_METHOD_ID_GPU_SET_PPAB_POWERLIMIT 4
// configurable TGP (power)
#define WMI_METHOD_ID_GPU_GET_CTGP_POWERLIMIT 5
#define WMI_METHOD_ID_GPU_SET_CTGP_POWERLIMIT 6
// ppab/ctgp powerlimit
#define WMI_METHOD_ID_GPU_GET_DEFAULT_PPAB_CTGP_POWERLIMIT 7
// temperature limit
#define WMI_METHOD_ID_GPU_GET_TEMPERATURE_LIMIT 8
#define WMI_METHOD_ID_GPU_SET_TEMPERATURE_LIMIT 9
// boost clock
#define WMI_METHOD_ID_GPU_GET_BOOST_CLOCK 10

#define WMI_GUID_LENOVO_FAN_METHOD "92549549-4bde-4f06-ac04-ce8bf898dbaa"
// set fan to maximal speed; dust cleaning mode
// only works in custom power mode
#define WMI_METHOD_ID_FAN_GET_FULLSPEED 1
#define WMI_METHOD_ID_FAN_SET_FULLSPEED 2
// max speed of fan
#define WMI_METHOD_ID_FAN_GET_MAXSPEED 3
#define WMI_METHOD_ID_FAN_SET_MAXSPEED 4
// fan table in custom mode
#define WMI_METHOD_ID_FAN_GET_TABLE 5
#define WMI_METHOD_ID_FAN_SET_TABLE 6
// get speed of fans
#define WMI_METHOD_ID_FAN_GETCURRENTFANSPEED 7
// get temperatures of CPU and GPU used for controlling cooling
#define WMI_METHOD_ID_FAN_GETCURRENTSENSORTEMPERATURE 8

// do not implement following
// #define WMI_METHOD_ID_Fan_SetCurrentFanSpeed 9

#define LEGION_WMI_KBBACKLIGHT_GUID "8C5B9127-ECD4-4657-980F-851019F99CA5"
// access the keyboard backlight with 3 states
#define WMI_METHOD_ID_KBBACKLIGHTGET 0x1
#define WMI_METHOD_ID_KBBACKLIGHTSET 0x2

// new method in newer methods to get or set most of the values
// with the two methods GetFeatureValue or SetFeatureValue.
// They are called like GetFeatureValue(feature_id) where
// feature_id is a id for the feature
#define LEGION_WMI_LENOVO_OTHER_METHOD_GUID \
	"dc2a8805-3a8c-41ba-a6f7-092e0089cd3b"
#define WMI_METHOD_ID_GET_FEATURE_VALUE 17
#define WMI_METHOD_ID_SET_FEATURE_VALUE 18

enum OtherMethodFeature {
	OtherMethodFeature_U1 = 0x010000, //->PC00.LPCB.EC0.REJF
	OtherMethodFeature_U2 = 0x0F0000, //->C00.PEG1.PXP._STA?
	OtherMethodFeature_U3 = 0x030000, //->PC00.LPCB.EC0.FLBT?
	OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT = 0x01010000,
	OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT = 0x01020000,
	OtherMethodFeature_CPU_PEAK_POWER_LIMIT = 0x01030000,
	OtherMethodFeature_CPU_TEMPERATURE_LIMIT = 0x01040000,

	OtherMethodFeature_APU_PPT_POWER_LIMIT = 0x01050000,

	OtherMethodFeature_CPU_CROSS_LOAD_POWER_LIMIT = 0x01060000,
	OtherMethodFeature_CPU_L1_TAU = 0x01070000,

	OtherMethodFeature_GPU_POWER_BOOST = 0x02010000,
	OtherMethodFeature_GPU_cTGP = 0x02020000,
	OtherMethodFeature_GPU_TEMPERATURE_LIMIT = 0x02030000,
	OtherMethodFeature_GPU_POWER_TARGET_ON_AC_OFFSET_FROM_BASELINE =
		0x02040000,

	OtherMethodFeature_FAN_SPEED_1 = 0x04030001,
	OtherMethodFeature_FAN_SPEED_2 = 0x04030002,
	/* firmware fan 4, the third fan on Q7CN (WMAE -> EC FASF * 100) */
	OtherMethodFeature_FAN_SPEED_4 = 0x04030004,
	OtherMethodFeature_FAN_FULLSPEED = 0x04020000,

	/* power on when AC / a USB-PD charger is connected (Q7CN: EC
	 * EACS/ETCS, set through WSMI)
	 */
	OtherMethodFeature_INSTANT_BOOT_AC = 0x03010001,
	OtherMethodFeature_INSTANT_BOOT_USB_PD = 0x03010002,

	OtherMethodFeature_C_U1 = 0x05010000,
	OtherMethodFeature_TEMP_CPU = 0x05040000,
	OtherMethodFeature_TEMP_GPU = 0x05050000,
};

#define LEGION_WMI_CAPDATA01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"

struct capdata01 {
	u32 id;
	u32 supported;
	u32 default_value;
	u32 step;
	u32 min_value;
	u32 max_value;
};

#define MAX_CAPDATA_ENTRIES 80

/* LENOVO_DISCRETE_DATA instances are ACPI packages [IDs u32, Value u32]; same-ID instances form a set */
#define LEGION_WMI_DISCRETE_DATA_GUID "91433B17-B7B7-4640-BB40-34C67349FBEC"

struct discrete_data_entry {
	u32 id;
	u32 value;
};

#define MAX_DISCRETE_ENTRIES 64
#define MAX_DISCRETE_FEATURES 8

struct discrete_feature {
	u32 feature_id;
	int values[32];
	int count;
};

static int capdata_clamp(const struct capdata01 *cd, int value,
			 const struct discrete_feature *df)
{
	int i, best;

	if (!cd)
		return value;

	if (cd->step == 0) {
		if (!df || df->count < 1)
			return value;

		best = df->values[0];
		for (i = 1; i < df->count; i++) {
			if (abs(value - df->values[i]) < abs(value - best))
				best = df->values[i];
		}
		return best;
	}

	if (cd->step > 0) {
		int offset = value - cd->min_value;
		int snapped = cd->min_value +
			      DIV_ROUND_CLOSEST(offset, cd->step) * cd->step;
		value = snapped;
	}

	return clamp(value, (int)cd->min_value, (int)cd->max_value);
}

static ssize_t wmi_other_method_get_value(enum OtherMethodFeature feature_id,
					  int *value)
{
	struct acpi_buffer params;
	int error;
	unsigned long res;
	u32 param1 = feature_id;

	params.length = sizeof(param1);
	params.pointer = &param1;
	error = wmi_exec_int(LEGION_WMI_LENOVO_OTHER_METHOD_GUID, 0,
			     WMI_METHOD_ID_GET_FEATURE_VALUE, &params, &res);
	if (!error)
		*value = res;
	return error;
}

struct wmi_other_method_value {
	u32 feature_id;
	u32 value;
};

static ssize_t wmi_other_method_set_value(enum OtherMethodFeature feature_id,
					  int value, int *output)
{
	struct acpi_buffer params;
	int error;
	unsigned long res;
	struct wmi_other_method_value input = {
		.feature_id = feature_id,
		.value = value,
	};

	// WMI Call 0x12 (18) struct:
	//
	// CreateWordField (Arg2, Zero, TYP1)
	// CreateByteField (Arg2, 0x02, FEA1)
	// CreateByteField (Arg2, 0x03, DEV1)
	// CreateDWordField (Arg2, 0x04, DAT1)
	//
	//OtherMethodFeature_FAN_FULLSPEED = 0x04020000,
	// TYP0 = 0x0000
	// FEA0 = 0x02
	// DEV0 = 0x04
	// DAT1 = 0xXXXXXXXX = parameter to add
	params.length = sizeof(input);
	params.pointer = &input;
	error = wmi_exec_int(LEGION_WMI_LENOVO_OTHER_METHOD_GUID, 0,
			     WMI_METHOD_ID_SET_FEATURE_VALUE, &params, &res);
	if (error)
		pr_info("Error calling WMI Other Method Set Value: %d\n",
			error);
	else
		*output = res;
	return error;
}

/* =================================== */
/* EC RAM Access with memory mapped IO */
/* =================================== */

struct ecram_memoryio {
	// TODO: start of remapped memory in EC RAM is assumed to be 0
	// u16 ecram_start;

	// physical address of remapped IO, depends on model and firmware
	phys_addr_t physical_start;
	// start adress of region in ec memory
	phys_addr_t physical_ec_start;
	// virtual address of remapped IO
	u8 *virtual_start;
	// size of remapped access
	size_t size;
};

/**
 * physical_start : corresponds to EC RAM 0 inside EC
 * size: size of remapped region
 *
 * strong exception safety
 */
static ssize_t ecram_memoryio_init(struct ecram_memoryio *ec_memoryio,
				   phys_addr_t physical_start,
				   phys_addr_t physical_ec_start, size_t size)
{
	void *virtual_start = ioremap(physical_start, size);

	if (!IS_ERR_OR_NULL(virtual_start)) {
		ec_memoryio->virtual_start = virtual_start;
		ec_memoryio->physical_start = physical_start;
		ec_memoryio->physical_ec_start = physical_ec_start;
		ec_memoryio->size = size;
		pr_info("Successfully mapped embedded controller: 0x%llx (in RAM)/0x%llx (in EC) to virtual 0x%p\n",
			ec_memoryio->physical_start,
			ec_memoryio->physical_ec_start,
			ec_memoryio->virtual_start);
	} else {
		pr_info("Error mapping embedded controller memory at 0x%llx\n",
			physical_start);
		return -ENOMEM;
	}
	return 0;
}

static void ecram_memoryio_exit(struct ecram_memoryio *ec_memoryio)
{
	if (ec_memoryio->virtual_start != NULL) {
		pr_info("Unmapping embedded controller memory at 0x%llx (in RAM)/0x%llx (in EC) at virtual 0x%p\n",
			ec_memoryio->physical_start,
			ec_memoryio->physical_ec_start,
			ec_memoryio->virtual_start);
		iounmap(ec_memoryio->virtual_start);
		ec_memoryio->virtual_start = NULL;
	}
}

/* Read a byte from the EC RAM.
 *
 * Return status because of commong signature for alle
 * methods to access EC RAM.
 */
static ssize_t ecram_memoryio_read(const struct ecram_memoryio *ec_memoryio,
				   u16 ec_offset, u8 *value)
{
	if (ec_offset < ec_memoryio->physical_ec_start ||
	    ec_offset - ec_memoryio->physical_ec_start >= ec_memoryio->size) {
		pr_info("Unexpected read at offset 0x%x into EC RAM\n",
			ec_offset);
		return -EIO;
	}
	*value = *(ec_memoryio->virtual_start +
		   (ec_offset - ec_memoryio->physical_ec_start));
	return 0;
}

/* Write a byte to the EC RAM.
 *
 * Return status because of commong signature for alle
 * methods to access EC RAM.
 */
static __maybe_unused ssize_t ecram_memoryio_write(
	const struct ecram_memoryio *ec_memoryio, u16 ec_offset, u8 value)
{
	if (ec_offset < ec_memoryio->physical_ec_start ||
	    ec_offset - ec_memoryio->physical_ec_start >= ec_memoryio->size) {
		pr_info("Unexpected write at offset 0x%x into EC RAM\n",
			ec_offset);
		return -EIO;
	}
	*(ec_memoryio->virtual_start +
	  (ec_offset - ec_memoryio->physical_ec_start)) = value;
	return 0;
}

/* ================================= */
/* EC RAM Access with port-mapped IO */
/* ================================= */

/*
 * See datasheet of e.g. IT8502E/F/G, e.g.
 * 6.2 Plug and Play Configuration (PNPCFG)
 *
 * Depending on configured BARDSEL register
 * the ports
 *   ECRAM_PORTIO_ADDR_PORT and
 *   ECRAM_PORTIO_DATA_PORT
 * are configured.
 *
 * By performing IO on these ports one can
 * read/write to registers in the EC.
 *
 * "To access a register of PNPCFG, write target index to
 *  address port and access this PNPCFG register via
 *  data port" [datasheet, 6.2 Plug and Play Configuration]
 */

// IO ports used to write to communicate with embedded controller
// Start of used ports
#define ECRAM_PORTIO_START_PORT 0x4E
// Number of used ports
#define ECRAM_PORTIO_PORTS_SIZE 2
// Port used to specify address in EC RAM to read/write
// 0x4E/0x4F is the usual port for IO super controller
// 0x2E/0x2F also common (ITE can also be configured to use these)
#define ECRAM_PORTIO_ADDR_PORT 0x4E
// Port to send/receive the value to write/read
#define ECRAM_PORTIO_DATA_PORT 0x4F
// Name used to request ports
#define ECRAM_PORTIO_NAME "legion"

struct ecram_portio {
	/* protects read/write to EC RAM performed
	 * as a certain sequence of outb, inb
	 * commands on the IO ports. There can
	 * be at most one.
	 */
	struct mutex io_port_mutex;
};

static ssize_t ecram_portio_init(struct ecram_portio *ec_portio)
{
	if (!request_region(ECRAM_PORTIO_START_PORT, ECRAM_PORTIO_PORTS_SIZE,
			    ECRAM_PORTIO_NAME)) {
		pr_info("Cannot init ecram_portio the %x ports starting at %x\n",
			ECRAM_PORTIO_PORTS_SIZE, ECRAM_PORTIO_START_PORT);
		return -ENODEV;
	}
	//pr_info("Reserved %x ports starting at %x\n", ECRAM_PORTIO_PORTS_SIZE, ECRAM_PORTIO_START_PORT);
	mutex_init(&ec_portio->io_port_mutex);
	return 0;
}

static void ecram_portio_exit(struct ecram_portio *ec_portio)
{
	release_region(ECRAM_PORTIO_START_PORT, ECRAM_PORTIO_PORTS_SIZE);
}

/* Read a byte from the EC RAM.
 *
 * Return status because of commong signature for alle
 * methods to access EC RAM.
 */
static ssize_t ecram_portio_read(struct ecram_portio *ec_portio, u16 offset,
				 u8 *value)
{
	mutex_lock(&ec_portio->io_port_mutex);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x11, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	// TODO: no explicit cast between types seems to be sometimes
	// done and sometimes not
	outb((u8)((offset >> 8) & 0xFF), ECRAM_PORTIO_DATA_PORT);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x10, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	outb((u8)(offset & 0xFF), ECRAM_PORTIO_DATA_PORT);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x12, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	*value = inb(ECRAM_PORTIO_DATA_PORT);

	mutex_unlock(&ec_portio->io_port_mutex);
	return 0;
}

/* Write a byte to the EC RAM.
 *
 * Return status because of commong signature for alle
 * methods to access EC RAM.
 */
static ssize_t ecram_portio_write(struct ecram_portio *ec_portio, u16 offset,
				  u8 value)
{
	mutex_lock(&ec_portio->io_port_mutex);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x11, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	// TODO: no explicit cast between types seems to be sometimes
	// done and sometimes not
	outb((u8)((offset >> 8) & 0xFF), ECRAM_PORTIO_DATA_PORT);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x10, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	outb((u8)(offset & 0xFF), ECRAM_PORTIO_DATA_PORT);

	outb(0x2E, ECRAM_PORTIO_ADDR_PORT);
	outb(0x12, ECRAM_PORTIO_DATA_PORT);
	outb(0x2F, ECRAM_PORTIO_ADDR_PORT);
	outb(value, ECRAM_PORTIO_DATA_PORT);

	mutex_unlock(&ec_portio->io_port_mutex);
	// TODO: remove this
	//pr_info("Writing %d to addr %x\n", value, offset);
	return 0;
}

/* =================================== */
/* EC RAM Access                       */
/* =================================== */

struct ecram {
	struct ecram_portio portio;
};

static ssize_t ecram_init(struct ecram *ecram,
			  phys_addr_t memoryio_ec_physical_start,
			  size_t region_size)
{
	ssize_t err;

	err = ecram_portio_init(&ecram->portio);
	if (err) {
		pr_info("Failed ecram_portio_init\n");
		goto err_ecram_portio_init;
	}

	return 0;

err_ecram_portio_init:
	return err;
}

static void ecram_exit(struct ecram *ecram)
{
	pr_info("Unloading legion ecram\n");
	ecram_portio_exit(&ecram->portio);
	pr_info("Unloading legion ecram done\n");
}

/** Read from EC RAM
 * ecram_offset address on the EC
 */
static u8 ecram_read(struct ecram *ecram, u16 ecram_offset)
{
	u8 value = 0;
	int err;

	err = ecram_portio_read(&ecram->portio, ecram_offset, &value);
	if (err)
		pr_info("Error reading EC RAM at 0x%x.\n", ecram_offset);
	return value;
}

static void ecram_write(struct ecram *ecram, u16 ecram_offset, u8 value)
{
	int err;

	if (ec_readonly) {
		pr_info("Skipping writing EC RAM to 0x%x: Read-Only.\n",
			ecram_offset);
		return;
	}
	err = ecram_portio_write(&ecram->portio, ecram_offset, value);
	if (err)
		pr_info("Error writing EC RAM to 0x%x: Read-Only.\n",
			ecram_offset);
}

/* =============================== */
/* Reads from EC  */
/* ===============================  */

static u16 read_ec_id(struct ecram *ecram, const struct model_config *model)
{
	u8 id1 = ecram_read(ecram, model->registers->ECHIPID1);
	u8 id2 = ecram_read(ecram, model->registers->ECHIPID2);

	return (id1 << 8) + id2;
}

static u16 read_ec_version(struct ecram *ecram,
			   const struct model_config *model)
{
	u8 vers = ecram_read(ecram, model->registers->ECHIPVER);
	u8 debug = ecram_read(ecram, model->registers->ECDEBUG);

	return (vers << 8) + debug;
}

/* ============================= */
/* Data model for sensor values  */
/* ============================= */

struct sensor_values {
	u16 fan1_rpm; // current speed in rpm of fan 1
	u16 fan2_rpm; // current speed in rpm of fan2
	u16 fan1_target_rpm; // target speed in rpm of fan 1
	u16 fan2_target_rpm; // target speed in rpm of fan 2
	u8 cpu_temp_celsius; // cpu temperature in celcius
	u8 gpu_temp_celsius; // gpu temperature in celcius
	u8 ic_temp_celsius; // ic temperature in celcius
};

enum SENSOR_ATTR {
	SENSOR_CPU_TEMP_ID = 1,
	SENSOR_GPU_TEMP_ID = 2,
	SENSOR_IC_TEMP_ID = 3,
	SENSOR_FAN1_RPM_ID = 4,
	SENSOR_FAN2_RPM_ID = 5,
	SENSOR_FAN1_TARGET_RPM_ID = 6,
	SENSOR_FAN2_TARGET_RPM_ID = 7,
	SENSOR_FAN3_RPM_ID = 8,
	SENSOR_FAN4_RPM_ID = 9
};

/* ============================= */
/* Data model for fan curve      */
/* ============================= */

#define MAX_RPM 10000
/* Highest level index Fan_Set_Table takes on Legion Zone v3 firmware. */
#define MAX_FAN_LEVEL 10

enum fan_speed_unit {
	FAN_SPEED_UNIT_PERCENT = 1,
	FAN_SPEED_UNIT_PWM = 2,
	FAN_SPEED_UNIT_RPM_HUNDRED = 3,
	FAN_SPEED_UNIT_PERCENT_NEAREST = 4,
	/*
	 * Index 1..MAX_FAN_LEVEL into the firmware's per-level RPM table
	 * (WMI data block LENOVO_FAN_TABLE_DATA.FanTable_Data), the unit
	 * Lenovo Legion Toolkit uses for Fan_Set_Table on Legion Zone v3
	 * firmware (e.g. KWCN54WW). The driver never sends index 0, see
	 * fancurve_level_min. A valid level can itself map to zero RPM.
	 */
	FAN_SPEED_UNIT_LEVEL = 5,
};

/*
 * Lowest level Lenovo Legion Toolkit sends per curve point on Legion Zone
 * v3 firmware (GodModeControllerV2 GetMinimumFanTableAsync): level 0 is
 * never sent and the last two points stay at or above 3 and 5. The EC
 * does not range-check the table itself, so this is a safety policy,
 * not a firmware limit.
 */
static const u8 fancurve_level_min[MAXFANCURVESIZE] = { 1, 1, 1, 1, 1,
							1, 1, 1, 3, 5 };

struct fancurve_point {
	// rpm1 devided by 100
	u8 speed1;
	// rpm2 devided by 100
	u8 speed2;
	// >=2 , <=5 (lower is faster); must increase by level
	u8 accel;
	// >=2 , <=5 (lower is faster); must increase by level
	u8 decel;

	// min must be lower than or equal to max
	// last level max must be 127
	// <=127 cpu max temp for this level; must increase by level
	u8 cpu_max_temp_celsius;
	// <=127 cpu min temp for this level; must increase by level
	u8 cpu_min_temp_celsius;
	// <=127 gpu min temp for this level; must increase by level
	u8 gpu_max_temp_celsius;
	// <=127 gpu max temp for this level; must increase by level
	u8 gpu_min_temp_celsius;
	// <=127 ic max temp for this level; must increase by level
	u8 ic_max_temp_celsius;
	// <=127 ic max temp for this level; must increase by level
	u8 ic_min_temp_celsius;
};

enum FANCURVE_ATTR {
	FANCURVE_ATTR_PWM1 = 1,
	FANCURVE_ATTR_PWM2 = 2,
	FANCURVE_ATTR_CPU_TEMP = 3,
	FANCURVE_ATTR_CPU_HYST = 4,
	FANCURVE_ATTR_GPU_TEMP = 5,
	FANCURVE_ATTR_GPU_HYST = 6,
	FANCURVE_ATTR_IC_TEMP = 7,
	FANCURVE_ATTR_IC_HYST = 8,
	FANCURVE_ATTR_ACCEL = 9,
	FANCURVE_ATTR_DECEL = 10,
	FANCURVE_SIZE = 11,
	FANCURVE_MINIFANCURVE_ON_COOL = 12
};

/* Match the fields actually consumed by each read/write implementation. */
static bool fancurve_attr_supported(const struct model_config *model, int id)
{
	if (id == FANCURVE_SIZE)
		return model->access_method_fancurve != ACCESS_METHOD_NO_ACCESS;

	switch (model->access_method_fancurve) {
	case ACCESS_METHOD_EC:
		return true;
	case ACCESS_METHOD_EC2:
		return id <= FANCURVE_ATTR_GPU_HYST;
	case ACCESS_METHOD_EC3:
		return id <= FANCURVE_ATTR_IC_HYST;
	case ACCESS_METHOD_EC4:
		return id == FANCURVE_ATTR_PWM1 || id == FANCURVE_ATTR_PWM2 ||
		       id == FANCURVE_ATTR_CPU_TEMP ||
		       id == FANCURVE_ATTR_GPU_TEMP;
	case ACCESS_METHOD_WMI3:
		return id == FANCURVE_ATTR_PWM1;
	default:
		return false;
	}
}

// used for clearing table entries
static const struct fancurve_point fancurve_point_zero = { 0, 0, 0, 0, 0,
							   0, 0, 0, 0, 0 };

struct fancurve {
	struct fancurve_point points[MAXFANCURVESIZE];
	enum fan_speed_unit fan_speed_unit;
	u16 max_rpm;
	// number of points used; must be <= MAXFANCURVESIZE
	size_t size;
	// the point at which fans are run currently
	size_t current_point_i;
};

static void fancurve_init(struct fancurve *fancurve,
			  const struct model_config *model)
{
	memset(fancurve, 0, sizeof(*fancurve));
	fancurve->max_rpm = model->fan_max_rpm;
}

// validation functions

static bool fancurve_is_valid_min_temp(int min_temp)
{
	return min_temp >= 0 && min_temp <= 127;
}

static bool fancurve_is_valid_max_temp(int max_temp)
{
	return max_temp >= 0 && max_temp <= 127;
}

// setters with validation
// - make hwmon implementation easier
// - keep fancurve valid, otherwise EC will not properly control fan

static bool fancurve_set_speed_pwm(struct fancurve *fancurve, int point_id,
				   int fan_id, int value)
{
	u8 *speed;

	if (!(value >= 0 && value <= 255)) {
		pr_err("Value %d PWM not in allowed range to point with id %d",
		       value, point_id);
		return false;
	}
	if (!(point_id < fancurve->size && fan_id >= 0 && fan_id < 2)) {
		pr_err("Setting point id %d, fan id %d not valid for fancurve with size %ld",
		       point_id, fan_id, fancurve->size);
		return false;
	}
	speed = fan_id == 0 ? &fancurve->points[point_id].speed1 :
			      &fancurve->points[point_id].speed2;

	switch (fancurve->fan_speed_unit) {
	case FAN_SPEED_UNIT_PERCENT:
		*speed = clamp_t(u8, value * 100 / 255, 0, 255);
		return true;
	case FAN_SPEED_UNIT_PERCENT_NEAREST:
		*speed = clamp_t(u8, (value * 100 + (255 / 2)) / 255, 0, 100);
		return true;
	case FAN_SPEED_UNIT_PWM:
		*speed = clamp_t(u8, value, 0, 255);
		return true;
	case FAN_SPEED_UNIT_RPM_HUNDRED: {
		u32 max_rpm = fancurve->max_rpm ? fancurve->max_rpm : MAX_RPM;

		*speed = clamp_t(
			u8, (value * max_rpm + (100 * 255) - 1) / (100 * 255),
			0, 255);
		return true;
	}
	case FAN_SPEED_UNIT_LEVEL: {
		int level = DIV_ROUND_CLOSEST(value * MAX_FAN_LEVEL, 255);

		if (level < fancurve_level_min[point_id]) {
			pr_err("Level %d (pwm %d) is below the minimum %d for pwm1_auto_point%d_pwm\n",
			       level, value, fancurve_level_min[point_id],
			       point_id + 1);
			return false;
		}
		*speed = level;
		return true;
	}
	default:
		pr_info("No method to set for fan_speed_unit %d.",
			fancurve->fan_speed_unit);
		return false;
	}
	return false;
}

static bool fancurve_get_speed_pwm(const struct fancurve *fancurve,
				   int point_id, int fan_id, int *value)
{
	int speed;

	if (!(point_id < fancurve->size && fan_id >= 0 && fan_id < 2)) {
		pr_err("Reading point id %d, fan id %d not valid for fancurve with size %ld",
		       point_id, fan_id, fancurve->size);
		return false;
	}

	speed = fan_id == 0 ? fancurve->points[point_id].speed1 :
			      fancurve->points[point_id].speed2;

	switch (fancurve->fan_speed_unit) {
	case FAN_SPEED_UNIT_PERCENT:
	case FAN_SPEED_UNIT_PERCENT_NEAREST:
		*value = speed * 255 / 100;
		return true;
	case FAN_SPEED_UNIT_PWM:
		*value = speed;
		return true;
	case FAN_SPEED_UNIT_RPM_HUNDRED: {
		u32 max_rpm = fancurve->max_rpm ? fancurve->max_rpm : MAX_RPM;

		*value = speed * 255 * 100 / max_rpm;
		return true;
	}
	case FAN_SPEED_UNIT_LEVEL:
		*value = min_t(int, speed, MAX_FAN_LEVEL) * 255 / MAX_FAN_LEVEL;
		return true;
	default:
		pr_info("No method to get for fan_speed_unit %d.",
			fancurve->fan_speed_unit);
		return false;
	}
	return false;
}

// TODO: remove { ... } from single line if body

static bool fancurve_set_accel(struct fancurve *fancurve, int point_id,
			       int accel)
{
	bool valid = accel >= 2 && accel <= 5;

	if (valid)
		fancurve->points[point_id].accel = accel;
	return valid;
}

static bool fancurve_set_decel(struct fancurve *fancurve, int point_id,
			       int decel)
{
	bool valid = decel >= 2 && decel <= 5;

	if (valid)
		fancurve->points[point_id].decel = decel;
	return valid;
}

static bool fancurve_set_cpu_temp_max(struct fancurve *fancurve, int point_id,
				      int value)
{
	bool valid = fancurve_is_valid_max_temp(value);

	if (valid)
		fancurve->points[point_id].cpu_max_temp_celsius = value;

	return valid;
}

static bool fancurve_set_gpu_temp_max(struct fancurve *fancurve, int point_id,
				      int value)
{
	bool valid = fancurve_is_valid_max_temp(value);

	if (valid)
		fancurve->points[point_id].gpu_max_temp_celsius = value;
	return valid;
}

static bool fancurve_set_ic_temp_max(struct fancurve *fancurve, int point_id,
				     int value)
{
	bool valid = fancurve_is_valid_max_temp(value);

	if (valid)
		fancurve->points[point_id].ic_max_temp_celsius = value;
	return valid;
}

static bool fancurve_set_cpu_temp_min(struct fancurve *fancurve, int point_id,
				      int value)
{
	bool valid = fancurve_is_valid_max_temp(value);

	if (valid)
		fancurve->points[point_id].cpu_min_temp_celsius = value;
	return valid;
}

static bool fancurve_set_gpu_temp_min(struct fancurve *fancurve, int point_id,
				      int value)
{
	bool valid = fancurve_is_valid_min_temp(value);

	if (valid)
		fancurve->points[point_id].gpu_min_temp_celsius = value;
	return valid;
}

static bool fancurve_set_ic_temp_min(struct fancurve *fancurve, int point_id,
				     int value)
{
	bool valid = fancurve_is_valid_min_temp(value);

	if (valid)
		fancurve->points[point_id].ic_min_temp_celsius = value;
	return valid;
}

static bool fancurve_set_size(struct fancurve *fancurve, int size,
			      bool init_values)
{
	bool valid = size >= 1 && size <= MAXFANCURVESIZE;

	if (!valid)
		return false;
	if (init_values && size < fancurve->size) {
		// fancurve size is decreased, but last entry always needs 127 temperatures
		// Note: size >=1
		fancurve->points[size - 1].cpu_max_temp_celsius = 127;
		fancurve->points[size - 1].ic_max_temp_celsius = 127;
		fancurve->points[size - 1].gpu_max_temp_celsius = 127;
	}
	if (init_values && size > fancurve->size) {
		// fancurve increased, so new entries need valid values
		int i;
		int last = fancurve->size > 0 ? fancurve->size - 1 : 0;

		for (i = fancurve->size; i < size; ++i)
			fancurve->points[i] = fancurve->points[last];
	}
	// The blocks above compare against the *old* size, so only publish the new
	// one once they are done. Without this the store is accepted, points[] is
	// reshaped, but fancurve->size - and therefore EXT_FAN_POINTS_SIZE - keeps
	// the value the EC reported, making a resize a silent no-op.
	fancurve->size = size;
	return true;
}

static ssize_t fancurve_print_seqfile(const struct fancurve *fancurve,
				      struct seq_file *s)
{
	int i;

	seq_printf(s, "Fan curve current point id: %ld\n",
		   fancurve->current_point_i);
	seq_printf(s, "Fan curve points size: %ld\n", fancurve->size);

	seq_printf(
		s,
		"u(speed_of_unit)|speed1[u]|speed2[u]|speed1[pwm]|speed2[pwm]|acceleration|deceleration|cpu_min_temp|cpu_max_temp|gpu_min_temp|gpu_max_temp|ic_min_temp|ic_max_temp\n");
	for (i = 0; i < fancurve->size; ++i) {
		int speed_pwm1 = -1;
		int speed_pwm2 = -1;
		const struct fancurve_point *point = &fancurve->points[i];

		fancurve_get_speed_pwm(fancurve, i, 0, &speed_pwm1);
		fancurve_get_speed_pwm(fancurve, i, 1, &speed_pwm2);

		seq_printf(
			s,
			"%d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\t %d\n",
			fancurve->fan_speed_unit, point->speed1, point->speed2,
			speed_pwm1, speed_pwm2, point->accel, point->decel,
			point->cpu_min_temp_celsius,
			point->cpu_max_temp_celsius,
			point->gpu_min_temp_celsius,
			point->gpu_max_temp_celsius, point->ic_min_temp_celsius,
			point->ic_max_temp_celsius);
	}
	return 0;
}

struct light {
	bool initialized;
	struct led_classdev led;
	unsigned int last_brightness;
	u8 light_id;
	unsigned int lower_limit;
	unsigned int upper_limit;
};

/* =============================  */
/* Global and shared data between */
/* all calls to this module       */
/* =============================  */
// Implemented like ideapad-laptop.c but currently still
// without dynamic memory allocation (instead global _priv)
/*
 * Rows of the LENOVO_FAN_TABLE_DATA WMI data block carry the firmware's
 * per-level RPM table; known models always have 10 levels (Lenovo
 * Legion Toolkit rejects tables with a different length).
 */
#define FANTABLE_MAX_LEVELS 10

/*
 * Per-fan RPM ladder for one power mode, derived from a matching row of
 * the LENOVO_FAN_TABLE_DATA WMI data block (see the fantable section
 * below for the row layout and refresh logic).
 */
struct fantable_ladder {
	u16 rpms[FANTABLE_MAX_LEVELS];
	/* current_fan_max_speed from the row, or the ladder's top RPM */
	u16 max_rpm;
	u8 level_count;
};

struct legion_private {
	struct platform_device *platform_device;
	// TODO: remove or keep? init?
	struct acpi_device *adev;

	// Method to access ECRAM
	struct ecram ecram;
	// Configuration with registers and ECRAM access method
	const struct model_config *conf;

	// TODO: maybe refactor and keep only local to each function
	// last known fan curve
	struct fancurve fancurve;
	// true if fancurve contains a valid curve (read or written)
	bool fancurve_valid;
	// configured fan curve from user space
	struct fancurve fancurve_configured;

	// update lock, when partial values of fancurve are changed
	struct mutex fancurve_mutex;

	//interfaces
	struct dentry *debugfs_dir;
	struct device *hwmon_dev;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	struct device *ppdev;
#else
	struct platform_profile_handler platform_profile_handler;
#endif

	struct light kbd_bl;
	struct light ylogo_light;
	struct light iport_light;

	// TODO: remove?
	bool loaded;

	// TODO: remove, only for reverse enginnering
	struct ecram_memoryio ec_memoryio;

	// PL1/PL2 coupling: runtime toggle, gated by model_config.has_pl_coupling
	bool cpu_pl_coupling;

	struct capdata01 capdata[MAX_CAPDATA_ENTRIES];
	int capdata_count;
	int current_powermode;

	/* last fan table written through hwmon, for restore_fancurve_on_resume */
	struct fancurve resume_fancurve;
	int resume_fancurve_mode;
	bool resume_fancurve_valid;
	struct delayed_work resume_fancurve_work;

	// Fan ceiling unlock state (cached: firmware exposes no clean read-back)
	u8 fan_unlock_state;

	struct discrete_feature discrete_features[MAX_DISCRETE_FEATURES];
	int discrete_feature_count;

	/* LENOVO_FAN_TABLE_DATA cache (models with has_fancurve_defaults) */
	struct fantable_ladder fantable_fan1;
	struct fantable_ladder fantable_fan2;
	bool fantable_fan1_valid;
	bool fantable_fan2_valid;
	int fantable_powermode;
};

// keep state of fancurve defaults powermode
static int fancurve_defaults_powermode;

// shared between different drivers: WMI, platform and protected by mutex
static struct legion_private *legion_shared;
static struct legion_private _priv;
static DEFINE_MUTEX(legion_shared_mutex);

static int legion_shared_init(struct legion_private *priv)
{
	int ret;

	mutex_lock(&legion_shared_mutex);

	if (!legion_shared) {
		legion_shared = priv;
		mutex_init(&legion_shared->fancurve_mutex);
		priv->fancurve_valid = false;
		ret = 0;
	} else {
		pr_warn("Found multiple platform devices\n");
		ret = -EINVAL;
	}

	priv->loaded = true;
	mutex_unlock(&legion_shared_mutex);

	return ret;
}

static void legion_shared_exit(struct legion_private *priv)
{
	pr_info("Unloading legion shared\n");
	mutex_lock(&legion_shared_mutex);

	if (legion_shared == priv)
		legion_shared = NULL;

	mutex_unlock(&legion_shared_mutex);
	pr_info("Unloading legion shared done\n");
}

static int get_simple_wmi_attribute(struct legion_private *priv,
				    const char *guid, u8 instance,
				    u32 method_id, bool invert,
				    unsigned long scale, unsigned long *value)
{
	unsigned long state = 0;
	int err;

	if (scale == 0) {
		pr_info("Scale cannot be 0\n");
		return -EINVAL;
	}
	err = wmi_exec_noarg_int(guid, instance, method_id, &state);
	if (err)
		return -EINVAL;

	// TODO: remove later
	pr_info("%swith raw value: %ld\n", __func__, state);

	state = state * scale;

	if (invert)
		state = !state;
	*value = state;
	return 0;
}

static int get_simple_wmi_attribute_bool(struct legion_private *priv,
					 const char *guid, u8 instance,
					 u32 method_id, bool invert,
					 unsigned long scale, bool *value)
{
	unsigned long int_val = 0;
	int err = get_simple_wmi_attribute(priv, guid, instance, method_id,
					   invert, scale, &int_val);
	if (!err)
		*value = int_val;
	return err;
}

static int set_simple_wmi_attribute(struct legion_private *priv,
				    const char *guid, u8 instance,
				    u32 method_id, bool invert, int scale,
				    unsigned int state)
{
	int err;
	u8 in_param;

	if (scale == 0) {
		pr_info("Scale cannot be 0\n");
		return -EINVAL;
	}

	if (invert)
		state = !state;

	// state is passed to the firmware as a u8 (state / scale); reject
	// inputs that would silently truncate instead of writing garbage.
	if (scale * 255U < state)
		return -EINVAL;

	in_param = state / scale;

	err = wmi_exec_arg(guid, instance, method_id, &in_param,
			   sizeof(in_param));
	return err;
}

/* ============================= */
/* Sensor value reading/writing */
/* ============================= */

// Temperature registers the driver has always read, independent of model.
#define EC_TEMP_INPUT_CPU 0xC5E6
#define EC_TEMP_INPUT_GPU 0xC5E7
#define EC_TEMP_INPUT_IC 0xC5E8

// Selects which per-model EXT_*_TEMP_INPUT offsets are trusted, see
// model_config.validated_temp_registers.
#define TEMP_REGISTER_CPU (1 << 0)
#define TEMP_REGISTER_GPU (1 << 1)
#define TEMP_REGISTER_IC (1 << 2)

// Pick the register to read a temperature from: the model's own offset once
// that register is validated for the model, otherwise the address the driver
// has always used.
static u16 temp_input_register(const struct model_config *model, u8 bit,
			       u16 model_register, u16 fallback)
{
	if (model->validated_temp_registers & bit)
		return model_register;
	return fallback;
}

static int ec_read_sensor_values(struct ecram *ecram,
				 const struct model_config *model,
				 struct sensor_values *values)
{
	int fan_target_mult = model->fan_target_is_duty ?
				      100 :
				      (model->acpi_fanspeed_is_rpm ? 1 : 100);

	values->fan1_target_rpm =
		fan_target_mult *
		ecram_read(ecram, model->registers->EXT_FAN1_TARGET_RPM);
	values->fan2_target_rpm =
		fan_target_mult *
		ecram_read(ecram, model->registers->EXT_FAN2_TARGET_RPM);

	values->fan1_rpm =
		ecram_read(ecram, model->registers->EXT_FAN1_RPM_LSB) +
		(((int)ecram_read(ecram, model->registers->EXT_FAN1_RPM_MSB))
		 << 8);
	values->fan2_rpm =
		ecram_read(ecram, model->registers->EXT_FAN2_RPM_LSB) +
		(((int)ecram_read(ecram, model->registers->EXT_FAN2_RPM_MSB))
		 << 8);

	values->cpu_temp_celsius = ecram_read(
		ecram, temp_input_register(model, TEMP_REGISTER_CPU,
					   model->registers->EXT_CPU_TEMP_INPUT,
					   EC_TEMP_INPUT_CPU));
	values->gpu_temp_celsius = ecram_read(
		ecram, temp_input_register(model, TEMP_REGISTER_GPU,
					   model->registers->EXT_GPU_TEMP_INPUT,
					   EC_TEMP_INPUT_GPU));
	values->ic_temp_celsius = ecram_read(
		ecram, temp_input_register(model, TEMP_REGISTER_IC,
					   model->registers->EXT_IC_TEMP_INPUT,
					   EC_TEMP_INPUT_IC));

	return 0;
}

static ssize_t ec_read_temperature(struct ecram *ecram,
				   const struct model_config *model,
				   int sensor_id, int *temperature)
{
	unsigned long res;

	if (sensor_id == 0) {
		res = ecram_read(ecram,
				 temp_input_register(
					 model, TEMP_REGISTER_CPU,
					 model->registers->EXT_CPU_TEMP_INPUT,
					 EC_TEMP_INPUT_CPU));
	} else if (sensor_id == 1) {
		res = ecram_read(ecram,
				 temp_input_register(
					 model, TEMP_REGISTER_GPU,
					 model->registers->EXT_GPU_TEMP_INPUT,
					 EC_TEMP_INPUT_GPU));
	} else {
		// TODO: use all correct error codes
		return -EEXIST;
	}
	*temperature = res;
	return 0;
}

static ssize_t ec_read_fanspeed(struct ecram *ecram,
				const struct model_config *model, int fan_id,
				int *fanspeed_rpm)
{
	unsigned long res;

	if (fan_id == 0) {
		res = ecram_read(ecram, model->registers->EXT_FAN1_RPM_LSB) +
		      (((int)ecram_read(ecram,
					model->registers->EXT_FAN1_RPM_MSB))
		       << 8);
	} else if (fan_id == 1) {
		res = ecram_read(ecram, model->registers->EXT_FAN2_RPM_LSB) +
		      (((int)ecram_read(ecram,
					model->registers->EXT_FAN2_RPM_MSB))
		       << 8);
	} else {
		// TODO: use all correct error codes
		return -EEXIST;
	}
	*fanspeed_rpm = res;
	return 0;
}

// '\_SB.PCI0.LPC0.EC0.FANS
// #define ACPI_PATH_FAN_SPEED1 "FANS"
// '\_SB.PCI0.LPC0.EC0.FA2S
// #define ACPI_PATH_FAN_SPEED2 "FA2S"
// '\_SB.PCI0.LPC0.EC0.FA3S
#define ACPI_PATH_FAN_SPEED3 "FA3S"
// '\_SB.PCI0.LPC0.EC0.FA4S
#define ACPI_PATH_FAN_SPEED4 "FA4S"

static ssize_t acpi_read_fanspeed(struct legion_private *priv, int fan_id,
				  int *value)
{
	int err;
	unsigned long acpi_value;
	const char *acpi_path;

	if (fan_id == 0) {
		acpi_path =
			get_model_acpi_path(_model, ACPI_PATH_READ_FANSPEED1);
	} else if (fan_id == 1) {
		acpi_path =
			get_model_acpi_path(_model, ACPI_PATH_READ_FANSPEED2);
	} else if (fan_id == 2) {
		acpi_path = ACPI_PATH_FAN_SPEED3;
	} else if (fan_id == 3) {
		acpi_path = ACPI_PATH_FAN_SPEED4;
	} else {
		return -EINVAL;
	}
	err = eval_int(priv->adev, acpi_path, &acpi_value);
	if (!err)
		*value = priv->conf->acpi_fanspeed_is_rpm ?
				 (int)acpi_value :
				 (int)acpi_value * 100;
	return err;
}

// '\_SB.PCI0.LPC0.EC0.CPUT
// #define ACPI_PATH_CPU_TEMP "CPUT"
// '\_SB.PCI0.LPC0.EC0.GPUT
// #define ACPI_PATH_GPU_TEMP "GPUT"

static ssize_t acpi_read_temperature(struct legion_private *priv, int fan_id,
				     int *value)
{
	int err;
	unsigned long acpi_value;
	const char *acpi_path;

	if (fan_id == 0) {
		acpi_path =
			get_model_acpi_path(_model, ACPI_PATH_READ_CPU_TEMP);
	} else if (fan_id == 1) {
		acpi_path =
			get_model_acpi_path(_model, ACPI_PATH_READ_GPU_TEMP);
	} else {
		// TODO: use all correct error codes
		return -EEXIST;
	}
	err = eval_int(priv->adev, acpi_path, &acpi_value);
	if (!err)
		*value = (int)acpi_value;
	return err;
}

// fan_id: 0 or 1
static ssize_t wmi_read_fanspeed(int fan_id, int *fanspeed_rpm)
{
	int err;
	unsigned long res;
	struct acpi_buffer params;

	params.length = 1;
	params.pointer = &fan_id;

	err = wmi_exec_int(WMI_GUID_LENOVO_FAN_METHOD, 0,
			   WMI_METHOD_ID_FAN_GETCURRENTFANSPEED, &params, &res);

	if (!err)
		*fanspeed_rpm = res;
	return err;
}

//sensor_id: cpu = 0, gpu = 1
static ssize_t wmi_read_temperature(int sensor_id, int *temperature)
{
	int err;
	unsigned long res;
	struct acpi_buffer params;

	if (sensor_id == 0)
		sensor_id = 0x03;
	else if (sensor_id == 1)
		sensor_id = 0x04;
	else {
		// TODO: use all correct error codes
		return -EEXIST;
	}

	params.length = 1;
	params.pointer = &sensor_id;

	err = wmi_exec_int(WMI_GUID_LENOVO_FAN_METHOD, 0,
			   WMI_METHOD_ID_FAN_GETCURRENTSENSORTEMPERATURE,
			   &params, &res);

	if (!err)
		*temperature = res;
	return err;
}

// fan_id: 0 or 1
static ssize_t wmi_read_fanspeed_gz(int fan_id, int *fanspeed_rpm)
{
	int err;
	u32 method_id;
	unsigned long res;

	if (fan_id == 0)
		method_id = WMI_METHOD_ID_GETFAN1SPEED;
	else if (fan_id == 1)
		method_id = WMI_METHOD_ID_GETFAN2SPEED;
	else {
		// TODO: use all correct error codes
		return -EEXIST;
	}
	err = wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0, method_id, &res);

	if (!err)
		*fanspeed_rpm = res;
	return err;
}

//sensor_id: cpu = 0, gpu = 1
static ssize_t wmi_read_temperature_gz(int sensor_id, int *temperature)
{
	int err;
	u32 method_id;
	unsigned long res;

	if (sensor_id == 0)
		method_id = WMI_METHOD_ID_GETCPUTEMP;
	else if (sensor_id == 1)
		method_id = WMI_METHOD_ID_GETGPUTEMP;
	else {
		// TODO: use all correct error codes
		return -EEXIST;
	}

	err = wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0, method_id, &res);

	if (!err)
		*temperature = res;
	return err;
}

// fan_id: 0, 1 or 2 (the third fan, firmware fan 4)
static ssize_t wmi_read_fanspeed_other(int fan_id, int *fanspeed_rpm)
{
	int err;
	enum OtherMethodFeature featured_id;
	int res;

	if (fan_id == 0)
		featured_id = OtherMethodFeature_FAN_SPEED_1;
	else if (fan_id == 1)
		featured_id = OtherMethodFeature_FAN_SPEED_2;
	else if (fan_id == 2)
		featured_id = OtherMethodFeature_FAN_SPEED_4;
	else {
		// TODO: use all correct error codes
		return -EEXIST;
	}

	err = wmi_other_method_get_value(featured_id, &res);

	if (!err)
		*fanspeed_rpm = res;
	return err;
}

//sensor_id: cpu = 0, gpu = 1
static ssize_t wmi_read_temperature_other(int sensor_id, int *temperature)
{
	int err;
	enum OtherMethodFeature featured_id;
	int res;

	if (sensor_id == 0)
		featured_id = OtherMethodFeature_TEMP_CPU;
	else if (sensor_id == 1)
		featured_id = OtherMethodFeature_TEMP_GPU;
	else {
		// TODO: use all correct error codes
		return -EEXIST;
	}

	err = wmi_other_method_get_value(featured_id, &res);
	if (!err)
		*temperature = res;
	return err;
}

static ssize_t read_fanspeed(struct legion_private *priv, int fan_id,
			     int *speed_rpm)
{
	// TODO: use enums or function pointers?
	switch (priv->conf->access_method_fanspeed) {
	case ACCESS_METHOD_EC:
		return ec_read_fanspeed(&priv->ecram, priv->conf, fan_id,
					speed_rpm);
	case ACCESS_METHOD_ACPI:
		return acpi_read_fanspeed(priv, fan_id, speed_rpm);
	case ACCESS_METHOD_WMI:
		return wmi_read_fanspeed_gz(fan_id, speed_rpm);
	case ACCESS_METHOD_WMI2:
		return wmi_read_fanspeed(fan_id, speed_rpm);
	case ACCESS_METHOD_WMI3:
		return wmi_read_fanspeed_other(fan_id, speed_rpm);
	default:
		pr_info("No access method for fanspeed: %d\n",
			priv->conf->access_method_fanspeed);
		return -EINVAL;
	}
}

static ssize_t read_temperature(struct legion_private *priv, int sensor_id,
				int *temperature)
{
	// TODO: use enums or function pointers?
	switch (priv->conf->access_method_temperature) {
	case ACCESS_METHOD_EC:
		return ec_read_temperature(&priv->ecram, priv->conf, sensor_id,
					   temperature);
	case ACCESS_METHOD_ACPI:
		return acpi_read_temperature(priv, sensor_id, temperature);
	case ACCESS_METHOD_WMI:
		return wmi_read_temperature_gz(sensor_id, temperature);
	case ACCESS_METHOD_WMI2:
		return wmi_read_temperature(sensor_id, temperature);
	case ACCESS_METHOD_WMI3:
		return wmi_read_temperature_other(sensor_id, temperature);
	default:
		pr_info("No access method for temperature: %d\n",
			priv->conf->access_method_temperature);
		return -EINVAL;
	}
}

/* ============================= */
/* Fancurve reading/writing      */
/* ============================= */

/* Fancurve from WMI
 * This allows changing fewer parameters.
 * It is only available on newer models.
 */

struct wmi_fan_table_read {
	__le32 fan_table_length;
	__le32 fan_speed[MAXFANCURVESIZE];
	__le32 sensor_table_length;
	__le32 sensor_value[MAXFANCURVESIZE];
} __packed;

static enum fan_speed_unit
wmi_fancurve_speed_unit(const struct model_config *model)
{
	return model == &model_n2cn	 ? FAN_SPEED_UNIT_PERCENT_NEAREST :
	       model == &model_secn	 ? FAN_SPEED_UNIT_RPM_HUNDRED :
	       model == &model_kwcn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_q7cn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_q6cn_lu	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_q6cn_f3	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_q6cn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_rxcn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_s2cn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_recn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_rlcn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_rgcn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_r3cn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_t2cn	 ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_nscn_83fd ? FAN_SPEED_UNIT_LEVEL :
	       model == &model_m3cn_8227 ? FAN_SPEED_UNIT_LEVEL :
					   FAN_SPEED_UNIT_PERCENT;
}

static ssize_t wmi_read_fancurve_custom(const struct model_config *model,
					struct fancurve *fancurve)
{
	struct wmi_fan_table_read fan_table;
	u8 params_data[2] = { 0, 0 };
	struct acpi_buffer params = {
		.length = sizeof(params_data),
		.pointer = params_data,
	};
	size_t size;
	size_t i;
	int err;

	// Pass two zero parameter bytes: required by some firmwares
	// (e.g. NXCN / Legion 9 16IRX9, see #393), ignored by others.
	err = wmi_exec_ints(WMI_GUID_LENOVO_FAN_METHOD, 0,
			    WMI_METHOD_ID_FAN_GET_TABLE, &params,
			    (u8 *)&fan_table, sizeof(fan_table));
	if (err)
		return err;

	if (model == &model_n2cn) {
		size = min_t(u32, le32_to_cpu(fan_table.fan_table_length),
			     MAXFANCURVESIZE);
		if (!size)
			return -EIO;
	} else {
		size = MAXFANCURVESIZE;
	}

	fancurve_init(fancurve, model);
	fancurve->current_point_i = 0;
	fancurve->size = size;
	fancurve->fan_speed_unit = wmi_fancurve_speed_unit(model);

	for (i = 0; i < size; i++) {
		u32 speed = le32_to_cpu(fan_table.fan_speed[i]);

		if (speed > U8_MAX)
			return -ERANGE;
		if (fancurve->fan_speed_unit == FAN_SPEED_UNIT_LEVEL &&
		    speed > MAX_FAN_LEVEL)
			pr_warn_once(
				"fan table point %zu holds %u, above level %d (stale percent write?)\n",
				i + 1, speed, MAX_FAN_LEVEL);
		fancurve->points[i].speed1 = speed;
		/* Fan_Set_Table carries one table for all fans (FSID = 0). */
		fancurve->points[i].speed2 = speed;
	}

	return 0;
}

/* ================================ */
/* WMI fan table data (per-level    */
/* fan RPM ladders)                 */
/* ================================ */

/*
 * LENOVO_FAN_TABLE_DATA (WMI data block GUID; the ACPI query method is
 * \_SB_.GZFD.WQA3/WQA7/... depending on the DSDT) publishes the
 * firmware's per-level RPM table: one row per power mode, fan and
 * sensor. Row layout as in PR #509 (struct WMIFanTableDefaultData,
 * validated on LZCN) and the fields Lenovo Legion Toolkit reads via
 * WQL. Tables always have 10 levels, so rows are parsed with an
 * exact-size struct through wmi_exec_query_ints(), which rejects rows
 * whose buffer size does not match.
 */
#define WMI_GUID_LENOVO_FANTABLE_DATA "87FB2A6D-D802-48E7-9208-4576C5F5C8D8"

#define FANTABLE_SENSOR_IC 0x01
#define FANTABLE_SENSOR_CPU 0x04
#define FANTABLE_SENSOR_GPU 0x05

struct wmi_fantable_row {
	u16 mode;
	u16 fan_id;
	u32 fan_table_len;
	u16 fan_speed[FANTABLE_MAX_LEVELS];
	u32 sensor_id;
	u32 sensor_table_len;
	u16 sensor_temp[FANTABLE_MAX_LEVELS];
	u8 start_only_upward_adjust_nbr;
	u8 end_only_upward_adjust_nbr;
	u16 current_fan_max_speed;
	u8 design_max_fan_speed_nbr;
	u8 reserved;
	u16 current_fan_min_speed;
	u16 fan_speed_step;
	u16 max_sensor_temp;
	u16 min_sensor_temp;
	u16 sensor_temp_step;
} __packed;

static int wmi_query_fantable_row(u8 index, struct wmi_fantable_row *row)
{
	int err = wmi_exec_query_ints(WMI_GUID_LENOVO_FANTABLE_DATA, index,
				      (u8 *)row, sizeof(*row));

	if (!err && (row->fan_table_len > FANTABLE_MAX_LEVELS ||
		     row->sensor_table_len > FANTABLE_MAX_LEVELS))
		return -ERANGE;

	return err;
}

/*
 * Entry i is firmware level i + 1, including zero-RPM or repeated steps.
 * Dropping a leading zero shifts every requested level (T2CN SFAN indexes
 * FNT[level + 2], while SFTW exports FNT[3..12] verbatim).
 */
static bool fantable_row_to_ladder(const struct wmi_fantable_row *row,
				   struct fantable_ladder *ladder)
{
	u32 count = row->fan_table_len;
	u8 i;

	if (count < 1 || count > FANTABLE_MAX_LEVELS)
		return false;

	for (i = 0; i < count; i++) {
		u16 rpm = row->fan_speed[i];

		if (i > 0 && rpm < ladder->rpms[i - 1])
			return false;
		ladder->rpms[i] = rpm;
	}
	if (!ladder->rpms[count - 1])
		return false;

	ladder->level_count = count;
	ladder->max_rpm = row->current_fan_max_speed;
	if (!ladder->max_rpm)
		ladder->max_rpm = ladder->rpms[count - 1];
	return true;
}

static bool fantable_row_matches_mode(const struct wmi_fantable_row *row,
				      int powermode)
{
	/* Standard mode rows carry the power mode id (1 quiet .. 3 perf). */
	if (row->mode == powermode)
		return true;
	/* Legion Zone firmware numbers custom mode rows as 0x100 and, by
	 * the same convention, extreme mode rows as 0xE1.
	 */
	if (powermode == 0xFF && row->mode == 0x100)
		return true;
	if (powermode == 0xE0 && row->mode == 0xE1)
		return true;
	return false;
}

/*
 * Rebuild the per-fan RPM ladders for the current power mode. Rows are
 * scanned by index until the block runs out of entries; the fan/sensor
 * pairs are the ones Lenovo Legion Toolkit documents. Only matching-mode
 * rows are usable: never substitute a different power mode's RPMs.
 */
static void fantable_refresh(struct legion_private *priv, int powermode)
{
	struct wmi_fantable_row row;
	struct fantable_ladder fan1_ic;
	bool fan1_ic_valid = false;
	int rows, index;

	priv->fantable_fan1_valid = false;
	priv->fantable_fan2_valid = false;

	/*
	 * The block holds one row per power mode, fan and state (AC/DC,
	 * hybrid mode, ...); the instance count is the exact row count
	 * (15 on current models, 50+ on some), so query it instead of
	 * scanning a fixed range.
	 */
	rows = wmi_instance_count(WMI_GUID_LENOVO_FANTABLE_DATA);
	if (rows <= 0)
		return;

	for (index = 0; index < rows; index++) {
		bool want_fan1, want_fan2;

		if (wmi_query_fantable_row(index, &row) ||
		    !fantable_row_matches_mode(&row, powermode))
			continue;

		want_fan1 = row.fan_id == 1 &&
			    row.sensor_id == FANTABLE_SENSOR_CPU;
		want_fan2 = row.fan_id == 2 &&
			    row.sensor_id == FANTABLE_SENSOR_GPU;
		if (want_fan1 && !priv->fantable_fan1_valid)
			priv->fantable_fan1_valid = fantable_row_to_ladder(
				&row, &priv->fantable_fan1);
		if (want_fan2 && !priv->fantable_fan2_valid)
			priv->fantable_fan2_valid = fantable_row_to_ladder(
				&row, &priv->fantable_fan2);
		/* Lenovo Legion Toolkit maps (fan 1, sensor 1) to the CPU fan
		 * as well; it is fan 1's only row on the Legion Pro 7 16IAX10H
		 * (Q7CN WQA3), where sensor 4 belongs to fan 4.
		 */
		if (row.fan_id == 1 && row.sensor_id == FANTABLE_SENSOR_IC &&
		    !fan1_ic_valid)
			fan1_ic_valid = fantable_row_to_ladder(&row, &fan1_ic);
	}

	/* Prefer the (fan 1, CPU sensor) row whenever the firmware has one. */
	if (!priv->fantable_fan1_valid && fan1_ic_valid) {
		priv->fantable_fan1 = fan1_ic;
		priv->fantable_fan1_valid = true;
	}

	if (priv->fantable_fan1_valid) {
		dev_info(&priv->platform_device->dev,
			 "fan table data: fan 1 has %u levels, %u..%u RPM\n",
			 priv->fantable_fan1.level_count,
			 priv->fantable_fan1.rpms[0],
			 priv->fantable_fan1
				 .rpms[priv->fantable_fan1.level_count - 1]);
	}
	if (priv->fantable_fan2_valid) {
		dev_info(&priv->platform_device->dev,
			 "fan table data: fan 2 has %u levels, %u..%u RPM\n",
			 priv->fantable_fan2.level_count,
			 priv->fantable_fan2.rpms[0],
			 priv->fantable_fan2
				 .rpms[priv->fantable_fan2.level_count - 1]);
	}

	priv->fantable_powermode = powermode;
}

static int sync_powermode_locked(struct legion_private *priv);
static int wmi_fancurve_mode(struct legion_private *priv);

/*
 * SFAN firmware that selects the RPM ladder from the payload mode byte and
 * drops writes in extreme mode (GZ44 == 7): the mode byte and the RPM
 * calibration must both follow the live thermal mode.
 */
static bool wmi_sfan_uses_thermal_mode(const struct model_config *conf)
{
	return conf == &model_t2cn || conf == &model_nscn_83fd;
}

/*
 * Return the ladders for the current power mode, refreshing the cache
 * when the power mode changed since the last scan. -ENODATA when the
 * block is unavailable or holds no usable row (transient failures keep
 * the cache invalid so the next call rescans). Caller holds
 * fancurve_mutex.
 */
static int fantable_ensure(struct legion_private *priv)
{
	int powermode, err;

	if (wmi_sfan_uses_thermal_mode(priv->conf)) {
		/* Calibration and SFAN must select the same live-mode ladder. */
		powermode = wmi_fancurve_mode(priv);
		if (powermode < 0)
			return powermode;
	} else {
		err = sync_powermode_locked(priv);
		if (err)
			return err;
		powermode = priv->current_powermode;
	}

	if (priv->fantable_fan1_valid && priv->fantable_fan2_valid &&
	    priv->fantable_powermode == powermode)
		return 0;

	fantable_refresh(priv, powermode);

	return priv->fantable_fan1_valid || priv->fantable_fan2_valid ?
		       0 :
		       -ENODATA;
}

/*
 * Make a level table safe to send. Fan_Get_Table returns ten zeros until
 * something has written the table (EC RAM untouched since boot), and an
 * older driver may have left percent-unit values above MAX_FAN_LEVEL
 * behind. Unset points get Legion Toolkit's default curve (1..10), the
 * rest is pulled inside [fancurve_level_min, MAX_FAN_LEVEL].
 */
static void fancurve_level_table_sanitize(u8 speeds[MAXFANCURVESIZE])
{
	size_t i;

	for (i = 0; i < MAXFANCURVESIZE; i++) {
		u8 fixed = speeds[i];

		if (fixed == 0)
			fixed = i + 1;
		fixed = clamp_t(u8, fixed, fancurve_level_min[i],
				MAX_FAN_LEVEL);
		if (fixed != speeds[i]) {
			pr_info("fan table point %zu: level %u sent as %u\n",
				i + 1, speeds[i], fixed);
			speeds[i] = fixed;
		}
	}
}

enum legion_wmi_powermode {
	LEGION_WMI_POWERMODE_LOW_POWER = 1,
	LEGION_WMI_POWERMODE_BALANCED = 2,
	LEGION_WMI_POWERMODE_PERFORMANCE = 3,
	LEGION_WMI_POWERMODE_CUSTOM = 255,
	LEGION_WMI_POWERMODE_MAX_POWER = 224
};

static ssize_t read_powermode(struct legion_private *priv, int *powermode);

static int read_fan_control_mode(struct legion_private *priv, int *powermode)
{
	unsigned long value;
	int err;

	if (!wmi_sfan_uses_thermal_mode(priv->conf) &&
	    priv->conf != &model_r3cn)
		return read_powermode(priv, powermode);

	/* SmartFanMode is a saved request; ThermalMode reads live GZ44. */
	err = wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0,
				 WMI_METHOD_ID_GETTHERMALMODE, &value);
	if (err)
		return err;
	if (value > U8_MAX)
		return -ERANGE;
	*powermode = value;
	return 0;
}

/* These SFAN implementations dereference an unset local for mode 0. */
static int wmi_fancurve_mode(struct legion_private *priv)
{
	int powermode, err;

	/* R3CN uses this only for WMI default restoration, not its EC3 curve. */
	if (priv->conf != &model_m3cn_8227 &&
	    !wmi_sfan_uses_thermal_mode(priv->conf) &&
	    priv->conf != &model_r3cn)
		return 0;

	err = read_fan_control_mode(priv, &powermode);
	if (err)
		return err;
	switch (powermode) {
	case LEGION_WMI_POWERMODE_LOW_POWER:
	case LEGION_WMI_POWERMODE_BALANCED:
	case LEGION_WMI_POWERMODE_PERFORMANCE:
	case LEGION_WMI_POWERMODE_CUSTOM:
		return powermode;
	default:
		/* Do not report success for ignored/unsupported mode writes. */
		return -EOPNOTSUPP;
	}
}

static ssize_t wmi_write_fancurve_custom(struct legion_private *priv,
					 const struct fancurve *fancurve)
{
	const struct model_config *model = priv->conf;
	u8 buffer[0x40];
	u8 speeds[MAXFANCURVESIZE];
	size_t point;
	int err;

	// The buffer is read like this in ACPI firmware
	//
	// CreateByteField (Arg2, Zero, FSTM)
	// CreateByteField (Arg2, One, FSID)
	// CreateDWordField (Arg2, 0x02, FSTL)
	// CreateByteField (Arg2, 0x06, FSS0)
	// CreateByteField (Arg2, 0x08, FSS1)
	// CreateByteField (Arg2, 0x0A, FSS2)
	// CreateByteField (Arg2, 0x0C, FSS3)
	// CreateByteField (Arg2, 0x0E, FSS4)
	// CreateByteField (Arg2, 0x10, FSS5)
	// CreateByteField (Arg2, 0x12, FSS6)
	// CreateByteField (Arg2, 0x14, FSS7)
	// CreateByteField (Arg2, 0x16, FSS8)
	// CreateByteField (Arg2, 0x18, FSS9)

	memset(buffer, 0, sizeof(buffer));
	err = wmi_fancurve_mode(priv);
	if (err < 0)
		return err;
	buffer[0] = err;
	for (point = 0; point < MAXFANCURVESIZE; point++)
		speeds[point] = fancurve->points[point].speed1;
	if (fancurve->fan_speed_unit == FAN_SPEED_UNIT_LEVEL)
		fancurve_level_table_sanitize(speeds);
	for (point = 0; point < MAXFANCURVESIZE; point++)
		buffer[0x06 + 2 * point] = speeds[point];

	print_hex_dump(KERN_DEBUG, "legion_laptop fan table wmi write buffer",
		       DUMP_PREFIX_ADDRESS, 16, 1, buffer, sizeof(buffer),
		       true);
	err = wmi_exec_arg(WMI_GUID_LENOVO_FAN_METHOD, 0,
			   WMI_METHOD_ID_FAN_SET_TABLE, buffer, sizeof(buffer));
	if (err || model != &model_n2cn)
		return err;

	{
		struct fancurve readback;
		size_t i;

		err = wmi_read_fancurve_custom(model, &readback);
		if (err)
			return err;
		for (i = 0; i < MAXFANCURVESIZE; i++) {
			if (readback.points[i].speed1 !=
			    fancurve->points[i].speed1)
				return -EIO;
		}
	}

	return err;
}

static ssize_t wmi_write_fancurve_defaults(struct legion_private *priv,
					   int value)
{
	int err = -1;
	struct {
		u8 F000; /* Thermal Mode/Powermode */
		u8 notused0[5]; /* F001 - F002 */
		u16 F003; /* Index Point 1 */
		u16 F004; /* 2 */
		u16 F005; /* 3 */
		u16 F006; /* 4 */
		u16 F007; /* 5 */
		u16 F008; /* 6 */
		u16 F009; /* 7 */
		u16 F00A; /* 8 */
		u16 F00B; /* 9 */
		u16 F00C; /* Index Point 10 */
		u8 notused1[26]; /* F00D - F019 */
	} __packed fan_table = { 0 };

	if (!priv->conf->has_fancurve_defaults) {
		pr_info("fancurve_defaults_powermode not supported for your model\n");
		return err;
	};
	err = wmi_fancurve_mode(priv);
	if (err < 0)
		return err;
	fan_table.F000 = value;
	fan_table.F003 = 0x01;
	fan_table.F004 = 0x02;
	fan_table.F005 = 0x03;
	fan_table.F006 = 0x04;
	fan_table.F007 = 0x05;
	fan_table.F008 = 0x06;
	fan_table.F009 = 0x07;
	fan_table.F00A = 0x08;
	fan_table.F00B = 0x09;
	fan_table.F00C = 0x0A;

	err = wmi_exec_arg(WMI_GUID_LENOVO_FAN_METHOD, 0,
			   WMI_METHOD_ID_FAN_SET_TABLE, (u8 *)&fan_table,
			   sizeof(fan_table));
	return err;
}

/* Read the fan curve from the EC.
 *
 * In newer models (>=2022) there is an ACPI/WMI to read fan curve as
 * a whole. So read/write fan table as a whole to use the
 * same interface for both cases.
 *
 * It reads all points from EC memory, even if stored fancurve is smaller, so
 * it can contain 0 entries.
 */
static int ec_read_fancurve_legion(struct ecram *ecram,
				   const struct model_config *model,
				   struct fancurve *fancurve)
{
	size_t i = 0;

	fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
	for (i = 0; i < MAXFANCURVESIZE; ++i) {
		struct fancurve_point *point = &fancurve->points[i];

		point->speed1 =
			ecram_read(ecram, model->registers->EXT_FAN1_BASE + i);
		point->speed2 =
			ecram_read(ecram, model->registers->EXT_FAN2_BASE + i);

		point->accel = ecram_read(
			ecram, model->registers->EXT_FAN_ACC_BASE + i);
		point->decel = ecram_read(
			ecram, model->registers->EXT_FAN_DEC_BASE + i);
		point->cpu_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_CPU_TEMP + i);
		point->cpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_CPU_TEMP_HYST + i);
		point->gpu_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_GPU_TEMP + i);
		point->gpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_GPU_TEMP_HYST + i);
		point->ic_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_VRM_TEMP + i);
		point->ic_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_VRM_TEMP_HYST + i);
	}

	// Do not trust that hardware; It might suddenly report
	// a larger size, so clamp it.
	fancurve->size =
		ecram_read(ecram, model->registers->EXT_FAN_POINTS_SIZE);
	fancurve->size =
		min(fancurve->size, (typeof(fancurve->size))(MAXFANCURVESIZE));
	fancurve->current_point_i =
		ecram_read(ecram, model->registers->EXT_FAN_CUR_POINT);
	fancurve->current_point_i =
		min(fancurve->current_point_i, fancurve->size);
	return 0;
}

static int ec_write_fancurve_legion(struct ecram *ecram,
				    const struct model_config *model,
				    const struct fancurve *fancurve,
				    bool write_size)
{
	size_t i;

	// Reset fan update counters (try to avoid any race conditions)
	ecram_write(ecram, 0xC5FE, 0);
	ecram_write(ecram, 0xC5FF, 0);
	for (i = 0; i < MAXFANCURVESIZE; ++i) {
		// Entries for points larger than fancurve size should be cleared
		// to 0
		const struct fancurve_point *point =
			i < fancurve->size ? &fancurve->points[i] :
					     &fancurve_point_zero;

		ecram_write(ecram, model->registers->EXT_FAN1_BASE + i,
			    point->speed1);
		ecram_write(ecram, model->registers->EXT_FAN2_BASE + i,
			    point->speed2);

		ecram_write(ecram, model->registers->EXT_FAN_ACC_BASE + i,
			    point->accel);
		ecram_write(ecram, model->registers->EXT_FAN_DEC_BASE + i,
			    point->decel);

		ecram_write(ecram, model->registers->EXT_CPU_TEMP + i,
			    point->cpu_max_temp_celsius);
		ecram_write(ecram, model->registers->EXT_CPU_TEMP_HYST + i,
			    point->cpu_min_temp_celsius);
		ecram_write(ecram, model->registers->EXT_GPU_TEMP + i,
			    point->gpu_max_temp_celsius);
		ecram_write(ecram, model->registers->EXT_GPU_TEMP_HYST + i,
			    point->gpu_min_temp_celsius);
		ecram_write(ecram, model->registers->EXT_VRM_TEMP + i,
			    point->ic_max_temp_celsius);
		ecram_write(ecram, model->registers->EXT_VRM_TEMP_HYST + i,
			    point->ic_min_temp_celsius);
	}

	if (write_size) {
		ecram_write(ecram, model->registers->EXT_FAN_POINTS_SIZE,
			    fancurve->size);
	}

	// Reset current fan level to 0, so algorithm in EC
	// selects fan curve point again and resetting hysterisis
	// effects
	ecram_write(ecram, model->registers->EXT_FAN_CUR_POINT, 0);

	// Reset internal fan levels
	ecram_write(ecram, 0xC634, 0); // CPU
	ecram_write(ecram, 0xC635, 0); // GPU
	ecram_write(ecram, 0xC636, 0); // SENSOR

	return 0;
}

#define FANCURVESIZE_IDEAPDAD 8

static int ec_read_fancurve_ideapad(struct ecram *ecram,
				    const struct model_config *model,
				    struct fancurve *fancurve)
{
	size_t i = 0;

	fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
	for (i = 0; i < FANCURVESIZE_IDEAPDAD; ++i) {
		struct fancurve_point *point = &fancurve->points[i];

		point->speed1 =
			ecram_read(ecram, model->registers->EXT_FAN1_BASE + i);
		point->speed2 =
			ecram_read(ecram, model->registers->EXT_FAN2_BASE + i);

		point->accel = 0;
		point->decel = 0;
		point->cpu_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_CPU_TEMP + i);
		point->cpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_CPU_TEMP_HYST + i);
		point->gpu_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_GPU_TEMP + i);
		point->gpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_GPU_TEMP_HYST + i);
		point->ic_max_temp_celsius = 0;
		point->ic_min_temp_celsius = 0;
	}

	// Do not trust that hardware; It might suddenly report
	// a larger size, so clamp it.
	fancurve->size = FANCURVESIZE_IDEAPDAD;
	fancurve->current_point_i =
		ecram_read(ecram, model->registers->EXT_FAN_CUR_POINT);
	fancurve->current_point_i =
		min(fancurve->current_point_i, fancurve->size);
	return 0;
}

static int ec_write_fancurve_ideapad(struct ecram *ecram,
				     const struct model_config *model,
				     const struct fancurve *fancurve)
{
	size_t i;
	int valr1;
	int valr2;

	// add this later: maybe other addresses needed
	// therefore, fan curve might not be effective immediately but
	// only after temp change
	// Reset fan update counters (try to avoid any race conditions)
	ecram_write(ecram, 0xC5FE, 0);
	ecram_write(ecram, 0xC5FF, 0);
	for (i = 0; i < FANCURVESIZE_IDEAPDAD; ++i) {
		const struct fancurve_point *point = &fancurve->points[i];

		ecram_write(ecram, model->registers->EXT_FAN1_BASE + i,
			    point->speed1);
		valr1 = ecram_read(ecram, model->registers->EXT_FAN1_BASE + i);
		ecram_write(ecram, model->registers->EXT_FAN2_BASE + i,
			    point->speed2);
		valr2 = ecram_read(ecram, model->registers->EXT_FAN2_BASE + i);
		pr_info("Writing fan1: %d; reading fan1: %d\n", point->speed1,
			valr1);
		pr_info("Writing fan2: %d; reading fan2: %d\n", point->speed2,
			valr2);

		// write to memory and repeat 8 bytes later again
		ecram_write(ecram, model->registers->EXT_CPU_TEMP + i,
			    point->cpu_max_temp_celsius);
		ecram_write(ecram, model->registers->EXT_CPU_TEMP + 8 + i,
			    point->cpu_max_temp_celsius);
		// write to memory and repeat 8 bytes later again
		ecram_write(ecram, model->registers->EXT_CPU_TEMP_HYST + i,
			    point->cpu_min_temp_celsius);
		ecram_write(ecram, model->registers->EXT_CPU_TEMP_HYST + 8 + i,
			    point->cpu_min_temp_celsius);
		// write to memory and repeat 8 bytes later again
		ecram_write(ecram, model->registers->EXT_GPU_TEMP + i,
			    point->gpu_max_temp_celsius);
		ecram_write(ecram, model->registers->EXT_GPU_TEMP + 8 + i,
			    point->gpu_max_temp_celsius);
		// write to memory and repeat 8 bytes later again
		ecram_write(ecram, model->registers->EXT_GPU_TEMP_HYST + i,
			    point->gpu_min_temp_celsius);
		ecram_write(ecram, model->registers->EXT_GPU_TEMP_HYST + 8 + i,
			    point->gpu_min_temp_celsius);
	}

	// add this later: maybe other addresses needed
	// therefore, fan curve might not be effective immediately but
	// only after temp change
	// // Reset current fan level to 0, so algorithm in EC
	// // selects fan curve point again and resetting hysterisis
	// // effects
	// ecram_write(ecram, model->registers->EXT_FAN_CUR_POINT, 0);

	// // Reset internal fan levels
	// ecram_write(ecram, 0xC634, 0); // CPU
	// ecram_write(ecram, 0xC635, 0); // GPU
	// ecram_write(ecram, 0xC636, 0); // SENSOR

	return 0;
}

#define FANCURVESIZE_LOQ 10

static int ec_read_fancurve_loq(struct ecram *ecram,
				const struct model_config *model,
				struct fancurve *fancurve)
{
	size_t i = 0;
	size_t struct_offset_ecram = 3;
	size_t struct_offset_ecramsys = 6;

	fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
	for (i = 0; i < FANCURVESIZE_LOQ; ++i) {
		struct fancurve_point *point = &fancurve->points[i];

		point->speed1 =
			ecram_read(ecram, model->registers->EXT_FAN1_RPM_LSB +
						  (i * struct_offset_ecram));
		point->speed2 =
			ecram_read(ecram, model->registers->EXT_FAN2_RPM_LSB +
						  (i * struct_offset_ecram));

		point->accel = 0;
		point->decel = 0;
		point->cpu_max_temp_celsius = ecram_read(
			ecram, model->registers->EXT_FAN1_RPM_LSB +
				       (i * struct_offset_ecram) - 1);
		point->cpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_FAN1_RPM_LSB +
				       (i * struct_offset_ecram) - 2);
		point->gpu_max_temp_celsius = ecram_read(
			ecram, model->registers->EXT_FAN2_RPM_LSB +
				       (i * struct_offset_ecram) - 1);
		point->gpu_min_temp_celsius = ecram_read(
			ecram, model->registers->EXT_FAN2_RPM_LSB +
				       (i * struct_offset_ecram) - 2);
		point->ic_max_temp_celsius =
			ecram_read(ecram, model->registers->EXT_VRM_TEMP +
						  (i * struct_offset_ecramsys));
		point->ic_min_temp_celsius =
			ecram_read(ecram, model->registers->EXT_VRM_TEMP_HYST +
						  (i * struct_offset_ecramsys));
	}

	fancurve->size = FANCURVESIZE_LOQ;
	fancurve->current_point_i =
		ecram_read(ecram, model->registers->EXT_FAN_CUR_POINT);
	fancurve->current_point_i =
		min(fancurve->current_point_i, fancurve->size);
	return 0;
}

#define LOQ_CMDR_ADDR 0xcfb6

static int ec_write_fancurve_loq(struct ecram *ecram,
				 const struct model_config *model,
				 const struct fancurve *fancurve)
{
	size_t i;
	int valr1;
	int valr2;
	u8 cmrd;
	size_t struct_offset_ecramsys = 6;

	for (i = 0; i < FANCURVESIZE_LOQ; ++i) {
		const struct fancurve_point *point = &fancurve->points[i];

		ecram_write(ecram,
			    model->registers->EXT_FAN1_BASE +
				    (i * struct_offset_ecramsys),
			    point->speed1);
		valr1 = ecram_read(ecram, model->registers->EXT_FAN1_BASE +
						  (i * struct_offset_ecramsys));

		ecram_write(ecram,
			    model->registers->EXT_FAN2_BASE +
				    (i * struct_offset_ecramsys),
			    point->speed2);
		valr2 = ecram_read(ecram, model->registers->EXT_FAN2_BASE +
						  (i * struct_offset_ecramsys));

		pr_info("Writing fan1: %d; reading fan1: %d\n", point->speed1,
			valr1);
		pr_info("Writing fan2: %d; reading fan2: %d\n", point->speed2,
			valr2);

		ecram_write(ecram,
			    model->registers->EXT_CPU_TEMP +
				    (i * struct_offset_ecramsys),
			    point->cpu_max_temp_celsius);
		ecram_write(ecram,
			    model->registers->EXT_CPU_TEMP_HYST +
				    (i * struct_offset_ecramsys),
			    point->cpu_min_temp_celsius);
		ecram_write(ecram,
			    model->registers->EXT_GPU_TEMP +
				    (i * struct_offset_ecramsys),
			    point->gpu_max_temp_celsius);
		ecram_write(ecram,
			    model->registers->EXT_GPU_TEMP_HYST +
				    (i * struct_offset_ecramsys),
			    point->gpu_min_temp_celsius);
		ecram_write(ecram,
			    model->registers->EXT_VRM_TEMP +
				    (i * struct_offset_ecramsys),
			    point->ic_max_temp_celsius);
		ecram_write(ecram,
			    model->registers->EXT_VRM_TEMP_HYST +
				    (i * struct_offset_ecramsys),
			    point->ic_min_temp_celsius);
	}
	// execute
	cmrd = ecram_read(ecram, LOQ_CMDR_ADDR);
	cmrd |= (1 << 4);
	ecram_write(ecram, LOQ_CMDR_ADDR, cmrd);

	return 0;
}

#define EC4_FANCURVE_SIZE 10
#define EC4_FAN1_BASE 0xC50A
#define EC4_FAN2_BASE 0xC531
#define EC4_POINT_STRIDE 3

static int ec_read_fancurve_legion2024(struct ecram *ecram,
				       const struct model_config *model,
				       struct fancurve *fancurve)
{
	int i;

	fancurve->fan_speed_unit = FAN_SPEED_UNIT_RPM_HUNDRED;
	fancurve->size = EC4_FANCURVE_SIZE;
	fancurve->current_point_i = 0;
	for (i = 0; i < EC4_FANCURVE_SIZE; i++) {
		struct fancurve_point *p = &fancurve->points[i];
		u8 off = EC4_POINT_STRIDE * i;

		p->cpu_max_temp_celsius =
			ecram_read(ecram, EC4_FAN1_BASE + off);
		p->gpu_max_temp_celsius =
			ecram_read(ecram, EC4_FAN1_BASE + off + 1);
		p->speed1 = ecram_read(ecram, EC4_FAN1_BASE + off + 2);
		p->speed2 = ecram_read(ecram, EC4_FAN2_BASE + off + 2);
		// Not stored in this EC layout
		p->cpu_min_temp_celsius = 0;
		p->gpu_min_temp_celsius = 0;
		p->ic_max_temp_celsius = 0;
		p->ic_min_temp_celsius = 0;
		p->accel = 0;
		p->decel = 0;
	}
	return 0;
}

static int ec_write_fancurve_legion2024(struct ecram *ecram,
					const struct model_config *model,
					const struct fancurve *fancurve)
{
	int i;

	for (i = 0; i < EC4_FANCURVE_SIZE; i++) {
		const struct fancurve_point *p = &fancurve->points[i];
		u8 off = EC4_POINT_STRIDE * i;

		// fan1 curve: cpu_max, gpu_max, speed1
		ecram_write(ecram, EC4_FAN1_BASE + off,
			    p->cpu_max_temp_celsius);
		ecram_write(ecram, EC4_FAN1_BASE + off + 1,
			    p->gpu_max_temp_celsius);
		ecram_write(ecram, EC4_FAN1_BASE + off + 2, p->speed1);
		// fan2 curve: same temperature thresholds, speed2
		ecram_write(ecram, EC4_FAN2_BASE + off,
			    p->cpu_max_temp_celsius);
		ecram_write(ecram, EC4_FAN2_BASE + off + 1,
			    p->gpu_max_temp_celsius);
		ecram_write(ecram, EC4_FAN2_BASE + off + 2, p->speed2);
	}
	return 0;
}

static int read_fancurve(struct legion_private *priv, struct fancurve *fancurve)
{
	int err;

	/* Native backends fill only the fields present in their EC layout. */
	fancurve_init(fancurve, priv->conf);

	// TODO: use enums or function pointers?
	switch (priv->conf->access_method_fancurve) {
	case ACCESS_METHOD_EC:
		err = ec_read_fancurve_legion(&priv->ecram, priv->conf,
					      fancurve);
		break;
	case ACCESS_METHOD_EC2:
		err = ec_read_fancurve_ideapad(&priv->ecram, priv->conf,
					       fancurve);
		break;
	case ACCESS_METHOD_EC3:
		err = ec_read_fancurve_loq(&priv->ecram, priv->conf, fancurve);
		break;
	case ACCESS_METHOD_EC4:
		err = ec_read_fancurve_legion2024(&priv->ecram, priv->conf,
						  fancurve);
		break;
	case ACCESS_METHOD_WMI3:
		err = wmi_read_fancurve_custom(priv->conf, fancurve);
		break;
	default:
		pr_info("No access method for fancurve: %d\n",
			priv->conf->access_method_fancurve);
		return -EINVAL;
	}

	if (!err) {
		priv->fancurve = *fancurve;
		priv->fancurve_valid = true;
	} else if (priv->fancurve_valid) {
		pr_info("Hardware fan curve read failed; returning cached curve\n");
		*fancurve = priv->fancurve;
		err = 0;
	}

	return err;
}

static int write_fancurve(struct legion_private *priv,
			  const struct fancurve *fancurve, bool write_size)
{
	int err;

	// TODO: use enums or function pointers?
	switch (priv->conf->access_method_fancurve) {
	case ACCESS_METHOD_EC:
		err = ec_write_fancurve_legion(&priv->ecram, priv->conf,
					       fancurve, write_size);
		break;
	case ACCESS_METHOD_EC2:
		err = ec_write_fancurve_ideapad(&priv->ecram, priv->conf,
						fancurve);
		break;
	case ACCESS_METHOD_EC3:
		err = ec_write_fancurve_loq(&priv->ecram, priv->conf, fancurve);
		break;
	case ACCESS_METHOD_EC4:
		err = ec_write_fancurve_legion2024(&priv->ecram, priv->conf,
						   fancurve);
		break;
	case ACCESS_METHOD_WMI3:
		err = wmi_write_fancurve_custom(priv, fancurve);
		break;
	default:
		pr_info("No access method for fancurve: %d\n",
			priv->conf->access_method_fancurve);
		return -EINVAL;
	}

	if (!err) {
		priv->fancurve = *fancurve;
		priv->fancurve_valid = true;
		/* remember it with its power mode for the resume re-apply */
		if (priv->conf->restore_fancurve_on_resume &&
		    !sync_powermode_locked(priv)) {
			priv->resume_fancurve = *fancurve;
			priv->resume_fancurve_mode = priv->current_powermode;
			priv->resume_fancurve_valid = true;
		}
	}

	return err;
}

/*
 * Delayed after resume, like legiond's resume hook: the EC applies its
 * per-point floor during wake-up and would overwrite an earlier write.
 * Only re-applied in the power mode the table was written in, since
 * another mode's table must not be replaced by it.
 */
static void legion_resume_fancurve_fn(struct work_struct *work)
{
	struct legion_private *priv = container_of(to_delayed_work(work),
						   struct legion_private,
						   resume_fancurve_work);
	struct device *dev = &priv->platform_device->dev;
	int err;

	mutex_lock(&priv->fancurve_mutex);
	if (!priv->resume_fancurve_valid) {
		mutex_unlock(&priv->fancurve_mutex);
		return;
	}
	err = sync_powermode_locked(priv);
	if (err || priv->current_powermode != priv->resume_fancurve_mode) {
		dev_info(dev,
			 "Fan curve not re-applied: mode %d, set in %d (%d)\n",
			 priv->current_powermode, priv->resume_fancurve_mode,
			 err);
		mutex_unlock(&priv->fancurve_mutex);
		return;
	}
	{
		struct fancurve fancurve = priv->resume_fancurve;

		err = write_fancurve(priv, &fancurve, false);
	}
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		dev_warn(dev, "Fan curve re-apply after resume failed: %d\n",
			 err);
	else
		dev_info(dev, "Re-applied the fan curve set before suspend\n");
}

/* the work checks resume_fancurve_valid under fancurve_mutex */
static void legion_resume_fancurve(struct legion_private *priv)
{
	if (priv && priv->conf && priv->conf->restore_fancurve_on_resume)
		schedule_delayed_work(&priv->resume_fancurve_work,
				      msecs_to_jiffies(3000));
}

static bool minifancurve_supported(const struct model_config *model)
{
	return model->has_minifancurve &&
	       model->registers->EXT_MINIFANCURVE_ON_COOL;
}

static bool lockfancontroller_supported(const struct model_config *model)
{
	return !model->skip_lockfancontroller &&
	       model->registers->EXT_LOCKFANCONTROLLER;
}

#define MINIFANCUVE_ON_COOL_ON 0x04
#define MINIFANCUVE_ON_COOL_OFF 0xA0

static int ec_read_minifancurve(struct ecram *ecram,
				const struct model_config *model, bool *state)
{
	int value;

	if (!minifancurve_supported(model))
		return -EOPNOTSUPP;
	value = ecram_read(ecram, model->registers->EXT_MINIFANCURVE_ON_COOL);

	switch (value) {
	case MINIFANCUVE_ON_COOL_ON:
		*state = true;
		break;
	case MINIFANCUVE_ON_COOL_OFF:
		*state = false;
		break;
	default:
		pr_info("Unexpected value in MINIFANCURVE register: %d\n",
			value);
		return -EIO;
	}
	return 0;
}

static ssize_t ec_write_minifancurve(struct ecram *ecram,
				     const struct model_config *model,
				     bool state)
{
	u8 val = state ? MINIFANCUVE_ON_COOL_ON : MINIFANCUVE_ON_COOL_OFF;

	if (!minifancurve_supported(model))
		return -EOPNOTSUPP;
	ecram_write(ecram, model->registers->EXT_MINIFANCURVE_ON_COOL, val);
	return 0;
}

#define EC_LOCKFANCONTROLLER_ON 8
#define EC_LOCKFANCONTROLLER_OFF 0

static ssize_t ec_write_lockfancontroller(struct ecram *ecram,
					  const struct model_config *model,
					  bool state)
{
	u8 val = state ? EC_LOCKFANCONTROLLER_ON : EC_LOCKFANCONTROLLER_OFF;

	if (!lockfancontroller_supported(model))
		return -EOPNOTSUPP;
	ecram_write(ecram, model->registers->EXT_LOCKFANCONTROLLER, val);
	return 0;
}

static int ec_read_lockfancontroller(struct ecram *ecram,
				     const struct model_config *model,
				     bool *state)
{
	int value;

	if (!lockfancontroller_supported(model))
		return -EOPNOTSUPP;
	value = ecram_read(ecram, model->registers->EXT_LOCKFANCONTROLLER);

	switch (value) {
	case EC_LOCKFANCONTROLLER_ON:
		*state = true;
		break;
	case EC_LOCKFANCONTROLLER_OFF:
		*state = false;
		break;
	default:
		pr_info("Unexpected value in lockfanspeed register: %d\n",
			value);
		return -EIO;
	}
	return 0;
}

static int fan_control_write_allowed(struct legion_private *priv)
{
	bool locked;
	int err;

	if (!priv->conf->require_unlocked_fan_controller)
		return 0;

	err = ec_read_lockfancontroller(&priv->ecram, priv->conf, &locked);
	if (err)
		return err;

	return locked ? -EBUSY : 0;
}

#define EC_FANFULLSPEED_ON 0x40
#define EC_FANFULLSPEED_OFF 0x00

static int ec_read_fanfullspeed(struct ecram *ecram,
				const struct model_config *model, bool *state)
{
	int value = ecram_read(ecram, model->registers->EXT_MAXIMUMFANSPEED);

	switch (value) {
	case EC_FANFULLSPEED_ON:
		*state = true;
		break;
	case EC_FANFULLSPEED_OFF:
		*state = false;
		break;
	default:
		pr_info("Unexpected value in maximumfanspeed register: %d\n",
			value);
		return -EIO;
	}
	return 0;
}

static ssize_t ec_write_fanfullspeed(struct ecram *ecram,
				     const struct model_config *model,
				     bool state)
{
	u8 val = state ? EC_FANFULLSPEED_ON : EC_FANFULLSPEED_OFF;

	ecram_write(ecram, model->registers->EXT_MAXIMUMFANSPEED, val);
	return 0;
}

#define EC4_FANFULLSPEED_REG 0xCFB6
#define EC4_FANFULLSPEED_FFON_BIT 0

static int ec_read_fanfullspeed_legion2024(struct ecram *ecram,
					   const struct model_config *model,
					   bool *state)
{
	int value = ecram_read(ecram, EC4_FANFULLSPEED_REG);

	*state = (value >> EC4_FANFULLSPEED_FFON_BIT) & 0x1;
	return 0;
}

static ssize_t
ec_write_fanfullspeed_legion2024(struct ecram *ecram,
				 const struct model_config *model, bool state)
{
	int value = ecram_read(ecram, EC4_FANFULLSPEED_REG);

	if (state)
		value |= (1 << EC4_FANFULLSPEED_FFON_BIT);
	else
		value &= ~(1 << EC4_FANFULLSPEED_FFON_BIT);
	ecram_write(ecram, EC4_FANFULLSPEED_REG, (u8)value);
	return 0;
}

static ssize_t wmi_read_fanfullspeed(struct legion_private *priv, bool *state)
{
	return get_simple_wmi_attribute_bool(priv, WMI_GUID_LENOVO_FAN_METHOD,
					     0, WMI_METHOD_ID_FAN_GET_FULLSPEED,
					     false, 1, state);
}

static ssize_t wmi_write_fanfullspeed(struct legion_private *priv, bool state)
{
	return set_simple_wmi_attribute(priv, WMI_GUID_LENOVO_FAN_METHOD, 0,
					WMI_METHOD_ID_FAN_SET_FULLSPEED, false,
					1, state);
}

static int wmi_read_fanfullspeed_other(struct legion_private *priv, bool *state)
{
	int err;
	int res;

	err = wmi_other_method_get_value(OtherMethodFeature_FAN_FULLSPEED,
					 &res);
	if (!err)
		*state = (res != 0); // ON
	return err;
}

static int wmi_write_fanfullspeed_other(struct legion_private *priv, bool state)
{
	int res;
	int value = (state) ? 1 : 0;

	return wmi_other_method_set_value(OtherMethodFeature_FAN_FULLSPEED,
					  value, &res);
}

static ssize_t read_fanfullspeed(struct legion_private *priv, bool *state)
{
	// TODO: use enums or function pointers?
	switch (priv->conf->access_method_fanfullspeed) {
	case ACCESS_METHOD_EC:
		return ec_read_fanfullspeed(&priv->ecram, priv->conf, state);
	case ACCESS_METHOD_EC4:
		return ec_read_fanfullspeed_legion2024(&priv->ecram, priv->conf,
						       state);
	case ACCESS_METHOD_WMI:
		return wmi_read_fanfullspeed(priv, state);
	case ACCESS_METHOD_WMI3:
		return wmi_read_fanfullspeed_other(priv, state);
	default:
		pr_info("No access method for fan full speed: %d\n",
			priv->conf->access_method_fanfullspeed);
		return -EINVAL;
	}
}

static ssize_t write_fanfullspeed(struct legion_private *priv, bool state)
{
	ssize_t res;

	switch (priv->conf->access_method_fanfullspeed) {
	case ACCESS_METHOD_EC:
		res = ec_write_fanfullspeed(&priv->ecram, priv->conf, state);
		return res;
	case ACCESS_METHOD_EC4:
		return ec_write_fanfullspeed_legion2024(&priv->ecram,
							priv->conf, state);
	case ACCESS_METHOD_WMI:
		return wmi_write_fanfullspeed(priv, state);
	case ACCESS_METHOD_WMI3:
		return wmi_write_fanfullspeed_other(priv, state);
	default:
		pr_info("No access method for fan full speed: %d\n",
			priv->conf->access_method_fanfullspeed);
		return -EINVAL;
	}
}

/* ============================= */
/* Power mode reading/writing    */
/* ============================= */

enum legion_ec_powermode {
	LEGION_EC_POWERMODE_QUIET = 2,
	LEGION_EC_POWERMODE_BALANCED = 0,
	LEGION_EC_POWERMODE_PERFORMANCE = 1,
	LEGION_EC_POWERMODE_CUSTOM = 3,
	LEGION_EC_POWERMODE_EXTREME = 7 // based on GZ44
};

static enum legion_wmi_powermode ec_to_wmi_powermode(int ec_mode)
{
	switch (ec_mode) {
	case LEGION_EC_POWERMODE_QUIET:
		return LEGION_WMI_POWERMODE_LOW_POWER;
	case LEGION_EC_POWERMODE_BALANCED:
		return LEGION_WMI_POWERMODE_BALANCED;
	case LEGION_EC_POWERMODE_PERFORMANCE:
		return LEGION_WMI_POWERMODE_PERFORMANCE;
	case LEGION_EC_POWERMODE_CUSTOM:
		return LEGION_WMI_POWERMODE_CUSTOM;
	case LEGION_EC_POWERMODE_EXTREME:
		return LEGION_WMI_POWERMODE_MAX_POWER;
	default:
		return LEGION_WMI_POWERMODE_BALANCED;
	}
}

static enum legion_ec_powermode
wmi_to_ec_powermode(enum legion_wmi_powermode wmi_mode)
{
	switch (wmi_mode) {
	case LEGION_WMI_POWERMODE_LOW_POWER:
		return LEGION_EC_POWERMODE_QUIET;
	case LEGION_WMI_POWERMODE_BALANCED:
		return LEGION_EC_POWERMODE_BALANCED;
	case LEGION_WMI_POWERMODE_PERFORMANCE:
		return LEGION_EC_POWERMODE_PERFORMANCE;
	case LEGION_WMI_POWERMODE_CUSTOM:
		return LEGION_EC_POWERMODE_CUSTOM;
	case LEGION_WMI_POWERMODE_MAX_POWER:
		return LEGION_EC_POWERMODE_EXTREME;
	default:
		return LEGION_EC_POWERMODE_BALANCED;
	}
}

static ssize_t ec_read_powermode(struct legion_private *priv, int *powermode)
{
	*powermode =
		ecram_read(&priv->ecram, priv->conf->registers->EXT_POWERMODE);
	return 0;
}

static ssize_t ec_write_powermode(struct legion_private *priv, u8 value)
{
	if (value != LEGION_EC_POWERMODE_BALANCED &&
	    value != LEGION_EC_POWERMODE_PERFORMANCE &&
	    value != LEGION_EC_POWERMODE_QUIET &&
	    value != LEGION_EC_POWERMODE_CUSTOM &&
	    value != LEGION_EC_POWERMODE_EXTREME) {
		pr_info("Unexpected power mode value ignored: %d\n", value);
		return -EINVAL;
	}
	ecram_write(&priv->ecram, priv->conf->registers->EXT_POWERMODE, value);
	return 0;
}

static ssize_t acpi_read_powermode(struct legion_private *priv, int *powermode)
{
	unsigned long acpi_powermode;
	int err;

	// spmo method not always available
	// \_SB.PCI0.LPC0.EC0.SPMO
	err = eval_spmo(priv->adev, &acpi_powermode);
	if (!err)
		*powermode = (int)acpi_powermode;
	return err;
}

static ssize_t wmi_read_powermode(int *powermode)
{
	int err;
	unsigned long res;

	err = wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0,
				 WMI_METHOD_ID_GETSMARTFANMODE, &res);

	if (!err)
		*powermode = res;
	return err;
}

static ssize_t wmi_write_powermode(u8 value)
{
	if (value != LEGION_WMI_POWERMODE_BALANCED &&
	    value != LEGION_WMI_POWERMODE_PERFORMANCE &&
	    value != LEGION_WMI_POWERMODE_LOW_POWER &&
	    value != LEGION_WMI_POWERMODE_CUSTOM &&
	    value != LEGION_WMI_POWERMODE_MAX_POWER) {
		pr_info("Unexpected power mode value ignored: %d\n", value);
		return -EINVAL;
	}
	return wmi_exec_arg(LEGION_WMI_GAMEZONE_GUID, 0,
			    WMI_METHOD_ID_SETSMARTFANMODE, &value,
			    sizeof(value));
}

static ssize_t read_powermode(struct legion_private *priv, int *powermode)
{
	ssize_t res;

	switch (priv->conf->access_method_powermode) {
	case ACCESS_METHOD_EC:
		res = ec_read_powermode(priv, powermode);
		*powermode = ec_to_wmi_powermode(*powermode);
		return res;
	case ACCESS_METHOD_ACPI:
		return acpi_read_powermode(priv, powermode);
	case ACCESS_METHOD_WMI:
		return wmi_read_powermode(powermode);
	default:
		pr_info("No access method for powermode: %d\n",
			priv->conf->access_method_powermode);
		return -EINVAL;
	}
}

/*
 * powermode_store() is the only mode write that also updates
 * priv->current_powermode; the mode changes without it too:
 * legion_platform_profile_set() writes it directly (platform_profile
 * sysfs, power-profiles-daemon), the mainline lenovo-wmi-gamezone
 * driver owns the platform profile when legion_laptop runs with
 * enable_platformprofile=0, and Fn+Q goes through lenovo-wmi-events
 * when both are loaded. So anything that selects per-mode firmware
 * data (capability rows for clamping, the fan table ladders) refreshes
 * the cache through this helper first. Caller holds fancurve_mutex, as
 * does powermode_store() when it writes the cache. On a read error the
 * cached value is kept. Consumers sync right before they use the value,
 * so a read that lands in the firmware's settling window after a mode
 * write only affects that one call.
 */
static int sync_powermode_locked(struct legion_private *priv)
{
	int powermode;
	int err = read_powermode(priv, &powermode);

	if (err < 0)
		return err;
	priv->current_powermode = powermode;
	return 0;
}

static ssize_t write_powermode(struct legion_private *priv,
			       enum legion_wmi_powermode value)
{
	ssize_t res;

	//TODO: remove again
	pr_info("Set powermode\n");

	switch (priv->conf->access_method_powermode) {
	case ACCESS_METHOD_EC:
		res = ec_write_powermode(priv, wmi_to_ec_powermode(value));
		return res;
	case ACCESS_METHOD_WMI:
		return wmi_write_powermode(value);
	default:
		pr_info("No access method for powermode: %d\n",
			priv->conf->access_method_powermode);
		return -EINVAL;
	}
}

/**
 * Shortly toggle powermode to a different mode
 * and switch back, e.g. to reset fan curve.
 */
static void toggle_powermode(struct legion_private *priv)
{
	int old_powermode;
	int next_powermode;
	int err;

	err = read_powermode(priv, &old_powermode);
	if (err) {
		pr_info("Failed to read powermode, skipping toggle: %d\n", err);
		return;
	}
	next_powermode = old_powermode == 0 ? 1 : 0;

	write_powermode(priv, next_powermode);
	mdelay(1500);
	write_powermode(priv, old_powermode);
}

/* ============================= */
/* Charging mode reading/writing */
/* ============================- */

#define FCT_RAPID_CHARGE_ON 0x07
#define FCT_RAPID_CHARGE_OFF 0x08
#define RAPID_CHARGE_ON 0x0
#define RAPID_CHARGE_OFF 0x1

#define FCT_CONSERVATION_ON 0x03
#define FCT_CONSERVATION_OFF 0x05
#define CONSERVATION_ON 0x0
#define CONSERVATION_OFF 0x1

static int acpi_read_rapidcharge(struct acpi_device *adev, bool *state)
{
	unsigned long result;
	int err;

	//also works? which one is better?
	/*
	 * err = eval_qcho(adev->handle, &result);
	 * if (err)
	 *  return err;
	 * state = result;
	 * return 0;
	 */

	err = eval_gbmd(adev, &result);
	if (err)
		return err;

	*state = result & 0x04;
	return 0;
}

static int acpi_write_rapidcharge(struct acpi_device *adev, bool state)
{
	int err;
	unsigned long fct_nr = state > 0 ? FCT_RAPID_CHARGE_ON :
					   FCT_RAPID_CHARGE_OFF;

	err = exec_sbmc(adev, fct_nr);
	pr_info("Set rapidcharge to %d by calling %lu: result: %d\n", state,
		fct_nr, err);
	return err;
}

static int acpi_read_conservation(struct acpi_device *adev, bool *state)
{
	unsigned long result;
	int err;

	err = eval_gbmd(adev, &result);
	if (err)
		return err;

	*state = result & 0x20;
	return 0;
}

static int acpi_write_conservation(struct acpi_device *adev, bool state)
{
	int err;
	unsigned long fct_nr = state > 0 ? FCT_CONSERVATION_ON :
					   FCT_CONSERVATION_OFF;

	err = exec_sbmc(adev, fct_nr);
	pr_info("Set conservation to %d by calling %lu: result: %d\n", state,
		fct_nr, err);
	return err;
}

/* ============================= */
/* Keyboard backlight read/write */
/* ============================= */

static ssize_t legion_kbd_bl2_brightness_get(struct legion_private *priv)
{
	unsigned long state = 0;
	int err;

	err = wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0,
				 WMI_METHOD_ID_GETKEYBOARDLIGHT, &state);
	if (err)
		return -EINVAL;

	return state;
}

//static int legion_kbd_bl2_brightness_set(struct legion_private *priv,
//					 unsigned int brightness)
//{
//	u8 in_param = brightness;

//	return wmi_exec_arg(LEGION_WMI_GAMEZONE_GUID, 0,
//			    WMI_METHOD_ID_SETKEYBOARDLIGHT, &in_param,
//			    sizeof(in_param));
//}

//min: 1, max: 3
#define LIGHT_ID_KEYBOARD 0x00
//min: 0, max: 1
#define LIGHT_ID_YLOGO 0x03
//min: 1, max: 2
#define LIGHT_ID_IOPORT 0x05

static int legion_wmi_light_get(struct legion_private *priv, u8 light_id,
				unsigned int min_value, unsigned int max_value)
{
	struct acpi_buffer params;
	u8 in;
	u8 result[2];
	u8 value;
	int err;

	params.length = 1;
	params.pointer = &in;
	in = light_id;
	err = wmi_exec_ints(LEGION_WMI_KBBACKLIGHT_GUID, 0,
			    WMI_METHOD_ID_KBBACKLIGHTGET, &params, result,
			    ARRAY_SIZE(result));
	if (err) {
		pr_info("Error for WMI method call to get brightness\n");
		return -EIO;
	}

	if (light_id == LIGHT_ID_YLOGO) {
		/* Y-Logo: DSDT returns on/off state in byte 0 (LCST).
		 * EC convention: 1 = ON, 2 = OFF.
		 * Map to sysfs brightness: 1 or 0.
		 */
		return (result[0] == 1) ? 1 : 0;
	}

	value = result[1];
	if (!(value >= min_value && value <= max_value)) {
		if (light_id == LIGHT_ID_IOPORT && value == 0)
			return -ENODEV;
		pr_info("Error WMI call for reading brightness: expected a value between %u and %u, but got %d\n",
			min_value, max_value, value);
		return -ERANGE;
	}

	return value - min_value;
}

static int legion_wmi_light_set(struct legion_private *priv, u8 light_id,
				unsigned int min_value, unsigned int max_value,
				unsigned int brightness)
{
	struct acpi_buffer buffer;
	u8 in_buffer_param[8];
	unsigned long result;
	int err;

	buffer.length = 3;
	buffer.pointer = &in_buffer_param[0];
	in_buffer_param[0] = light_id;
	if (light_id == LIGHT_ID_YLOGO) {
		/* Y-Logo: DSDT checks SCST (byte 1) for on/off.
		 * EC convention: 1 = ON, 2 = OFF.
		 */
		in_buffer_param[1] = brightness ? 1 : 2;
		in_buffer_param[2] = 0;
	} else {
		in_buffer_param[1] = 0x01;
		in_buffer_param[2] =
			clamp(brightness + min_value, min_value, max_value);
	}

	err = wmi_exec_int(LEGION_WMI_KBBACKLIGHT_GUID, 0,
			   WMI_METHOD_ID_KBBACKLIGHTSET, &buffer, &result);
	if (err) {
		pr_info("Error for WMI method call to set brightness on light: %d\n",
			light_id);
		return -EIO;
	}

	return 0;
}

static int legion_kbd_bl_brightness_get(struct legion_private *priv)
{
	return legion_wmi_light_get(priv, LIGHT_ID_KEYBOARD, 1, 3);
}

static int legion_kbd_bl_brightness_set(struct legion_private *priv,
					unsigned int brightness)
{
	return legion_wmi_light_set(priv, LIGHT_ID_KEYBOARD, 1, 3, brightness);
}

/* =============================  */
/* debugfs interface              */
/* ============================   */

static int debugfs_ecmemory_show(struct seq_file *s, void *unused)
{
	struct legion_private *priv = s->private;
	size_t offset;

	for (offset = 0; offset < priv->conf->memoryio_size; ++offset) {
		char value = ecram_read(&priv->ecram,
					priv->conf->memoryio_physical_ec_start +
						offset);

		seq_write(s, &value, 1);
	}
	return 0;
}

DEFINE_SHOW_ATTRIBUTE(debugfs_ecmemory);

static int debugfs_ecmemoryram_show(struct seq_file *s, void *unused)
{
	struct legion_private *priv = s->private;
	size_t offset;
	ssize_t err;
	u8 value;

	for (offset = 0; offset < priv->conf->ramio_size; ++offset) {
		err = ecram_memoryio_read(&priv->ec_memoryio, offset, &value);
		if (!err)
			seq_write(s, &value, 1);
		else
			return -EACCES;
	}
	return 0;
}

DEFINE_SHOW_ATTRIBUTE(debugfs_ecmemoryram);

//TODO: make (almost) all methods static

static void seq_file_print_with_error(struct seq_file *s, const char *name,
				      ssize_t err, int value)
{
	seq_printf(s, "%s error: %ld\n", name, err);
	if (!err)
		seq_printf(s, "%s: %d\n", name, value);
}

static int debugfs_fancurve_show(struct seq_file *s, void *unused)
{
	struct legion_private *priv = s->private;
	bool is_minifancurve = false;
	bool is_lockfancontroller = false;
	bool is_maximumfanspeed = false;
	bool is_rapidcharge = false;
	int powermode = 0;
	int temperature = 0;
	int fanspeed = 0;
	int err;
	unsigned long cfg;
	struct fancurve wmi_fancurve;
	//int kb_backlight;

	mutex_lock(&priv->fancurve_mutex);

	seq_printf(s, "EC Chip ID: %x\n", read_ec_id(&priv->ecram, priv->conf));
	seq_printf(s, "EC Chip Version: %x\n",
		   read_ec_version(&priv->ecram, priv->conf));
	seq_printf(s, "legion_laptop features: %s\n", LEGIONFEATURES);
	seq_printf(s, "legion_laptop ec_readonly: %d\n", ec_readonly);

	const char *acpi_path;

	acpi_path = get_model_acpi_path(_model, ACPI_PATH_CFG);
	err = eval_int(priv->adev, acpi_path, &cfg);
	seq_printf(s, "ACPI CFG error: %d\n", err);
	if (!err)
		seq_printf(s, "ACPI CFG: %lu\n", cfg);

	seq_printf(s, "temperature access method: %d\n",
		   priv->conf->access_method_temperature);
	err = read_temperature(priv, 0, &temperature);
	seq_file_print_with_error(s, "CPU temperature", err, temperature);
	err = ec_read_temperature(&priv->ecram, priv->conf, 0, &temperature);
	seq_file_print_with_error(s, "CPU temperature EC", err, temperature);
	err = acpi_read_temperature(priv, 0, &temperature);
	seq_file_print_with_error(s, "CPU temperature ACPI", err, temperature);
	err = wmi_read_temperature_gz(0, &temperature);
	seq_file_print_with_error(s, "CPU temperature WMI", err, temperature);
	err = wmi_read_temperature(0, &temperature);
	seq_file_print_with_error(s, "CPU temperature WMI2", err, temperature);
	err = wmi_read_temperature_other(0, &temperature);
	seq_file_print_with_error(s, "CPU temperature WMI3", err, temperature);

	err = read_temperature(priv, 1, &temperature);
	seq_file_print_with_error(s, "GPU temperature", err, temperature);
	err = ec_read_temperature(&priv->ecram, priv->conf, 1, &temperature);
	seq_file_print_with_error(s, "GPU temperature EC", err, temperature);
	err = acpi_read_temperature(priv, 1, &temperature);
	seq_file_print_with_error(s, "GPU temperature ACPI", err, temperature);
	err = wmi_read_temperature_gz(1, &temperature);
	seq_file_print_with_error(s, "GPU temperature WMI", err, temperature);
	err = wmi_read_temperature(1, &temperature);
	seq_file_print_with_error(s, "GPU temperature WMI2", err, temperature);
	err = wmi_read_temperature_other(1, &temperature);
	seq_file_print_with_error(s, "GPU temperature WMI3", err, temperature);

	seq_printf(s, "fan speed access method: %d\n",
		   priv->conf->access_method_fanspeed);
	err = read_fanspeed(priv, 0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed", err, fanspeed);
	err = ec_read_fanspeed(&priv->ecram, priv->conf, 0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed EC", err, fanspeed);
	err = acpi_read_fanspeed(priv, 0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed ACPI", err, fanspeed);
	err = wmi_read_fanspeed_gz(0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed WMI", err, fanspeed);
	err = wmi_read_fanspeed(0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed WMI2", err, fanspeed);
	err = wmi_read_fanspeed_other(0, &fanspeed);
	seq_file_print_with_error(s, "1 fanspeed WMI3", err, fanspeed);

	err = read_fanspeed(priv, 1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed", err, fanspeed);
	err = ec_read_fanspeed(&priv->ecram, priv->conf, 1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed EC", err, fanspeed);
	err = acpi_read_fanspeed(priv, 1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed ACPI", err, fanspeed);
	err = wmi_read_fanspeed_gz(1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed WMI", err, fanspeed);
	err = wmi_read_fanspeed(1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed WMI2", err, fanspeed);
	err = wmi_read_fanspeed_other(1, &fanspeed);
	seq_file_print_with_error(s, "2 fanspeed WMI3", err, fanspeed);

	seq_printf(s, "powermode access method: %d\n",
		   priv->conf->access_method_powermode);
	err = read_powermode(priv, &powermode);
	seq_file_print_with_error(s, "powermode", err, powermode);
	err = ec_read_powermode(priv, &powermode);
	seq_file_print_with_error(s, "powermode EC", err, powermode);
	err = acpi_read_powermode(priv, &powermode);
	seq_file_print_with_error(s, "powermode ACPI", err, powermode);
	err = wmi_read_powermode(&powermode);
	seq_file_print_with_error(s, "powermode WMI", err, powermode);
	seq_printf(s, "has custom powermode: %d\n",
		   priv->conf->has_custom_powermode);

	err = acpi_read_rapidcharge(priv->adev, &is_rapidcharge);
	seq_printf(s, "ACPI rapidcharge error: %d\n", err);
	seq_printf(s, "ACPI rapidcharge: %d\n", is_rapidcharge);

	seq_printf(s, "WMI backlight 2 state: %ld\n",
		   legion_kbd_bl2_brightness_get(priv));
	seq_printf(s, "WMI backlight 3 state: %d\n",
		   legion_kbd_bl_brightness_get(priv));

	seq_printf(s, "WMI light IO port: %d\n",
		   legion_wmi_light_get(priv, LIGHT_ID_IOPORT, 0, 4));

	seq_printf(s, "WMI light Y logo/lid: %d\n",
		   legion_wmi_light_get(priv, LIGHT_ID_YLOGO, 0, 4));

	seq_printf(s, "EC minifancurve feature enabled: %d\n",
		   minifancurve_supported(priv->conf));
	err = ec_read_minifancurve(&priv->ecram, priv->conf, &is_minifancurve);
	seq_printf(s, "EC minifancurve on cool: %s\n",
		   err ? "error" : (is_minifancurve ? "true" : "false"));

	err = ec_read_lockfancontroller(&priv->ecram, priv->conf,
					&is_lockfancontroller);
	seq_printf(s, "EC lockfancontroller error: %d\n", err);
	seq_printf(s, "EC lockfancontroller: %s\n",
		   err ? "error" : (is_lockfancontroller ? "true" : "false"));

	err = read_fanfullspeed(priv, &is_maximumfanspeed);
	seq_file_print_with_error(s, "fanfullspeed", err, is_maximumfanspeed);

	err = ec_read_fanfullspeed(&priv->ecram, priv->conf,
				   &is_maximumfanspeed);
	seq_file_print_with_error(s, "fanfullspeed EC", err,
				  is_maximumfanspeed);
	seq_printf(s, "Max speed for fancurve: %d\n", MAX_RPM);

	read_fancurve(priv, &priv->fancurve);

	seq_puts(s, "Current fan curve in hardware:\n");
	fancurve_print_seqfile(&priv->fancurve, s);
	seq_puts(s, "=====================\n");
	mutex_unlock(&priv->fancurve_mutex);

	seq_puts(s, "Current fan curve in hardware (WMI; might be empty)\n");
	wmi_fancurve.size = 0;
	err = wmi_read_fancurve_custom(priv->conf, &wmi_fancurve);
	if (err)
		seq_printf(s, "WMI fancurve error: %d\n", err);
	else
		fancurve_print_seqfile(&wmi_fancurve, s);
	seq_puts(s, "=====================\n");
	return 0;
}

DEFINE_SHOW_ATTRIBUTE(debugfs_fancurve);

static void legion_debugfs_init(struct legion_private *priv)
{
	struct dentry *dir;

	// TODO: remove this note
	// Note: like other kernel modules, do not catch errors here
	// because if kernel is build without debugfs this
	// will return an error but module still has to
	// work, just without debugfs
	// TODO: what permissions; some modules do 400
	// other do 444
	dir = debugfs_create_dir(LEGION_DRVR_SHORTNAME, NULL);
	debugfs_create_file("fancurve", 0444, dir, priv,
			    &debugfs_fancurve_fops);
	debugfs_create_file("ecmemory", 0444, dir, priv,
			    &debugfs_ecmemory_fops);
	debugfs_create_file("ecmemoryram", 0444, dir, priv,
			    &debugfs_ecmemoryram_fops);

	priv->debugfs_dir = dir;
}

static void legion_debugfs_exit(struct legion_private *priv)
{
	pr_info("Unloading legion dubugfs\n");
	// The following is does nothing if pointer is NULL
	debugfs_remove_recursive(priv->debugfs_dir);
	priv->debugfs_dir = NULL;
	pr_info("Unloading legion dubugfs done\n");
}

/* =============================  */
/* sysfs interface                */
/* ============================   */

static int show_simple_wmi_attribute(struct device *dev,
				     struct device_attribute *attr, char *buf,
				     const char *guid, u8 instance,
				     u32 method_id, bool invert,
				     unsigned long scale)
{
	unsigned long state = 0;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	mutex_lock(&priv->fancurve_mutex);
	err = get_simple_wmi_attribute(priv, guid, instance, method_id, invert,
				       scale, &state);
	mutex_unlock(&priv->fancurve_mutex);

	if (err)
		return err;

	return sysfs_emit(buf, "%lu\n", state);
}

static int show_simple_wmi_attribute_from_buffer(struct device *dev,
						 struct device_attribute *attr,
						 char *buf, const char *guid,
						 u8 instance, u32 method_id,
						 size_t ressize, size_t i,
						 int scale)
{
	int err;
	unsigned long value;
	struct legion_private *priv = dev_get_drvdata(dev);

	if (i >= ressize) {
		pr_info("Index not within buffer size\n");
		return -EINVAL;
	}

	mutex_lock(&priv->fancurve_mutex);
	err = wmi_exec_noarg_int_or_buffer(guid, instance, method_id, ressize,
					   i, &value);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return sysfs_emit(buf, "%lu\n", scale * value);
}

static int store_simple_wmi_attribute(struct device *dev,
				      struct device_attribute *attr,
				      const char *buf, size_t count,
				      const char *guid, u8 instance,
				      u32 method_id, bool invert, int scale)
{
	unsigned int state;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	err = kstrtouint(buf, 0, &state);
	if (err)
		return err;
	err = set_simple_wmi_attribute(priv, guid, instance, method_id, invert,
				       scale, state);
	if (err)
		return err;
	return count;
}

static ssize_t lockfancontroller_show(struct device *dev,
				      struct device_attribute *attr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool is_lockfancontroller;
	int err;

	mutex_lock(&priv->fancurve_mutex);
	err = ec_read_lockfancontroller(&priv->ecram, priv->conf,
					&is_lockfancontroller);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EINVAL;

	return sysfs_emit(buf, "%d\n", is_lockfancontroller);
}

static ssize_t lockfancontroller_store(struct device *dev,
				       struct device_attribute *attr,
				       const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool is_lockfancontroller;
	int err;

	err = kstrtobool(buf, &is_lockfancontroller);
	if (err)
		return err;

	mutex_lock(&priv->fancurve_mutex);
	err = ec_write_lockfancontroller(&priv->ecram, priv->conf,
					 is_lockfancontroller);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EINVAL;

	return count;
}

static DEVICE_ATTR_RW(lockfancontroller);

// Fan ceiling unlock — see WMI_METHOD_ID_FAN_EXTREME_TOGGLE comment for context.
// Cached state because the firmware exposes no clean read-back path; the value
// reflects the last value successfully written through this attribute.

static ssize_t fan_unlock_show(struct device *dev,
			       struct device_attribute *attr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	u8 state;

	mutex_lock(&priv->fancurve_mutex);
	state = priv->fan_unlock_state;
	mutex_unlock(&priv->fancurve_mutex);
	return sysfs_emit(buf, "%d\n", state);
}

static ssize_t fan_unlock_store(struct device *dev,
				struct device_attribute *attr, const char *buf,
				size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool enable;
	u8 arg;
	int err;

	err = kstrtobool(buf, &enable);
	if (err)
		return err;

	arg = enable ? 1 : 0;
	mutex_lock(&priv->fancurve_mutex);
	err = wmi_exec_arg(LEGION_WMI_GAMEZONE_GUID, 0,
			   WMI_METHOD_ID_FAN_EXTREME_TOGGLE, &arg, sizeof(arg));
	if (!err)
		priv->fan_unlock_state = arg;
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EIO;
	return count;
}

static DEVICE_ATTR_RW(fan_unlock);

static ssize_t rapidcharge_show(struct device *dev,
				struct device_attribute *attr, char *buf)
{
	bool state = false;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	mutex_lock(&priv->fancurve_mutex);
	err = acpi_read_rapidcharge(priv->adev, &state);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return sysfs_emit(buf, "%d\n", state);
}

static ssize_t rapidcharge_store(struct device *dev,
				 struct device_attribute *attr, const char *buf,
				 size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool state;
	int err;

	err = kstrtobool(buf, &state);
	if (err)
		return err;

	mutex_lock(&priv->fancurve_mutex);
	err = acpi_write_rapidcharge(priv->adev, state);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return count;
}

static DEVICE_ATTR_RW(rapidcharge);

static ssize_t battery_conservation_show(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	bool state = false;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	mutex_lock(&priv->fancurve_mutex);
	err = acpi_read_conservation(priv->adev, &state);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return sysfs_emit(buf, "%d\n", state);
}

static ssize_t battery_conservation_store(struct device *dev,
					  struct device_attribute *attr,
					  const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool state;
	int err;

	err = kstrtobool(buf, &state);
	if (err)
		return err;

	mutex_lock(&priv->fancurve_mutex);
	/*
	 * Conservation and rapid charge are mutually exclusive. The firmware
	 * clears conservation when rapid charge is enabled, but not the other
	 * way round; leaving both set makes ideapad-laptop's charge_types read
	 * fail with -EINVAL. Turn rapid charge off first, as ideapad-laptop
	 * does, and leave it alone when disabling conservation. If its state
	 * can't be read, leave it alone too and still set conservation; only
	 * a failed rapid charge write aborts.
	 */
	if (state) {
		bool rapid;

		if (!acpi_read_rapidcharge(priv->adev, &rapid) && rapid) {
			err = acpi_write_rapidcharge(priv->adev, false);
			if (err)
				goto unlock;
		}
	}
	err = acpi_write_conservation(priv->adev, state);
unlock:
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return count;
}

static DEVICE_ATTR_RW(battery_conservation);

static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr,
			    char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	unsigned long result;
	int err;

	mutex_lock(&priv->fancurve_mutex);
	err = eval_hals(priv->adev, &result);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return sysfs_emit(buf, "%d\n", !!(result & 0x0400));
}

static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr,
			     const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool enable;
	int err;

	if (kstrtobool(buf, &enable))
		return -EINVAL;

	mutex_lock(&priv->fancurve_mutex);
	err = exec_sals(priv->adev, enable ? 0x0E : 0x0F);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return count;
}

static DEVICE_ATTR_RW(fn_lock);

#define FBSWIF_GUID                                                        \
	EFI_GUID(0xD743491E, 0xF484, 0x4952, 0xA8, 0x7D, 0x8D, 0x5D, 0xD1, \
		 0x89, 0xB7, 0x0C)
#define FBSWIF_NAME L"FBSWIF"

static ssize_t flip_to_start_show(struct device *dev,
				  struct device_attribute *attr, char *buf)
{
	u8 data[4] = { 0 };
	efi_status_t status;
	unsigned long size = sizeof(data);
	u32 efi_attr;

	status = efi.get_variable(FBSWIF_NAME, &FBSWIF_GUID, &efi_attr, &size,
				  data);
	if (status != EFI_SUCCESS)
		return -EIO;

	return sysfs_emit(buf, "%d\n", data[0] ? 1 : 0);
}

static ssize_t flip_to_start_store(struct device *dev,
				   struct device_attribute *attr,
				   const char *buf, size_t count)
{
	bool state;
	int err;
	u8 data[4] = { 0 };
	efi_status_t status;
	unsigned long size;
	u32 efi_attr;

	err = kstrtobool(buf, &state);
	if (err)
		return err;

	size = sizeof(data);
	status = efi.get_variable(FBSWIF_NAME, &FBSWIF_GUID, &efi_attr, &size,
				  data);
	if (status != EFI_SUCCESS)
		return -EIO;

	data[0] = state ? 1 : 0;

	size = sizeof(data);
	status = efi.set_variable(FBSWIF_NAME, &FBSWIF_GUID, efi_attr, size,
				  data);
	if (status != EFI_SUCCESS)
		return -EIO;

	return count;
}

static DEVICE_ATTR_RW(flip_to_start);

static ssize_t issupportgpuoc_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_ISSUPPORTGPUOC, false,
					 1);
}

static DEVICE_ATTR_RO(issupportgpuoc);

static ssize_t aslcodeversion_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETVERSION, false, 1);
}

static DEVICE_ATTR_RO(aslcodeversion);

static ssize_t issupportcpuoc_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_ISSUPPORTCPUOC, false,
					 1);
}

static DEVICE_ATTR_RO(issupportcpuoc);

static ssize_t winkey_show(struct device *dev, struct device_attribute *attr,
			   char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETWINKEYSTATUS, true,
					 1);
}

static ssize_t winkey_store(struct device *dev, struct device_attribute *attr,
			    const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_SETWINKEYSTATUS, true,
					  1);
}

static DEVICE_ATTR_RW(winkey);

// on newer models the touchpad feature in ideapad does not work anymore, so
// we need this
static ssize_t touchpad_show(struct device *dev, struct device_attribute *attr,
			     char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETTPSTATUS, true, 1);
}

static ssize_t touchpad_store(struct device *dev, struct device_attribute *attr,
			      const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_SETTPSTATUS, true, 1);
}

static DEVICE_ATTR_RW(touchpad);

static ssize_t gsync_show(struct device *dev, struct device_attribute *attr,
			  char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETGSYNCSTATUS, true, 1);
}

static ssize_t gsync_store(struct device *dev, struct device_attribute *attr,
			   const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_SETGSYNCSTATUS, true,
					  1);
}

static DEVICE_ATTR_RW(gsync);

static ssize_t powerchargemode_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETPOWERCHARGEMODE,
					 false, 1);
}
static DEVICE_ATTR_RO(powerchargemode);

static ssize_t overdrive_show(struct device *dev, struct device_attribute *attr,
			      char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETODSTATUS, false, 1);
}

static ssize_t overdrive_store(struct device *dev,
			       struct device_attribute *attr, const char *buf,
			       size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_SETODSTATUS, false, 1);
}

static DEVICE_ATTR_RW(overdrive);

static ssize_t thermalmode_show(struct device *dev,
				struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETTHERMALMODE, false,
					 1);
}
static DEVICE_ATTR_RO(thermalmode);

// TOOD: probably remove again because provided by other means; only useful for overclocking
static ssize_t cpumaxfrequency_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETCPUMAXFREQUENCY,
					 false, 1);
}
static DEVICE_ATTR_RO(cpumaxfrequency);

static ssize_t isacfitforoc_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_ISACFITFOROC, false, 1);
}
static DEVICE_ATTR_RO(isacfitforoc);

static ssize_t igpumode_show(struct device *dev, struct device_attribute *attr,
			     char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_GETIGPUMODESTATUS, false,
					 1);
}

static ssize_t igpumode_store(struct device *dev, struct device_attribute *attr,
			      const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_SETIGPUMODESTATUS,
					  false, 1);
}

static DEVICE_ATTR_RW(igpumode);

static ssize_t notify_dgpu_store(struct device *dev,
				 struct device_attribute *attr, const char *buf,
				 size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  LEGION_WMI_GAMEZONE_GUID, 0,
					  WMI_METHOD_ID_NOTIFYDGPUSTATUS, false,
					  1);
}

static DEVICE_ATTR_WO(notify_dgpu);

static ssize_t issupportigpumode_show(struct device *dev,
				      struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_ISSUPPORTIGPUMODE, false,
					 1);
}

static DEVICE_ATTR_RO(issupportigpumode);

static ssize_t issupportgsync_show(struct device *dev,
				   struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 LEGION_WMI_GAMEZONE_GUID, 0,
					 WMI_METHOD_ID_ISSUPPORTGSYNC, false,
					 1);
}

static DEVICE_ATTR_RO(issupportgsync);

static ssize_t cpu_oc_show(struct device *dev, struct device_attribute *attr,
			   char *buf)
{
	return show_simple_wmi_attribute_from_buffer(
		dev, attr, buf, WMI_GUID_LENOVO_CPU_METHOD, 0,
		WMI_METHOD_ID_CPU_GET_OC_STATUS, 16, 0, 1);
}

static ssize_t cpu_oc_store(struct device *dev, struct device_attribute *attr,
			    const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_CPU_METHOD, 0,
					  WMI_METHOD_ID_CPU_SET_OC_STATUS,
					  false, 1);
}

static DEVICE_ATTR_RW(cpu_oc);

static ssize_t wmi_common_method_other_show(struct legion_private *priv,
					    char *buf, int feature_id)
{
	int err, out;

	mutex_lock(&priv->fancurve_mutex);
	err = wmi_other_method_get_value(feature_id, &out);
	mutex_unlock(&priv->fancurve_mutex);

	if (err)
		return -EINVAL;

	return sysfs_emit(buf, "%d\n", out);
}

/*
 * The capdata01 row for a feature (id bits 31..16) in a power mode (bits
 * 15..8) with sub-id 0 (bits 7..0), if the firmware marks it supported.
 * Shared by the write clamping and the exported per-mode defaults, so both
 * always agree on which row applies.
 */
static const struct capdata01 *
capdata01_lookup(const struct legion_private *priv, u32 fkey, int powermode)
{
	int i;

	for (i = 0; i < priv->capdata_count; i++) {
		const struct capdata01 *p = &priv->capdata[i];

		if ((p->id >> 16) == fkey &&
		    ((p->id >> 8) & 0xFF) == (u32)powermode &&
		    (p->id & 0xFF) == 0 && (p->supported & BIT(0)))
			return p;
	}
	return NULL;
}

static int clamped_value(struct legion_private *priv,
			 enum OtherMethodFeature feature, const char *buf,
			 int *value)
{
	const struct capdata01 *cd = NULL;
	const struct discrete_feature *df = NULL;
	u32 fkey = (u32)feature >> 16;
	int powermode;
	int i, err;

	err = kstrtoint(buf, 10, value);
	if (err)
		return err;

	/* Snapshot: powermode_store() may update the cache once we unlock. */
	mutex_lock(&priv->fancurve_mutex);
	sync_powermode_locked(priv);
	powermode = priv->current_powermode;
	mutex_unlock(&priv->fancurve_mutex);

	cd = capdata01_lookup(priv, fkey, powermode);

	for (i = 0; i < priv->discrete_feature_count; i++) {
		if ((priv->discrete_features[i].feature_id >> 16) == fkey) {
			df = &priv->discrete_features[i];
			break;
		}
	}

	*value = capdata_clamp(cd, *value, df);
	return 0;
}

static ssize_t wmi_common_method_other_store(struct legion_private *priv,
					     const char *buf, size_t count,
					     enum OtherMethodFeature feature_id)
{
	/*
	 * Writes are clamped to the ranges the firmware publishes in
	 * LENOVO_CAPABILITY_DATA_01 / LENOVO_DISCRETE_DATA for the current
	 * power mode; features without a capability row pass through
	 * unchanged (see clamped_value).
	 */
	int err, value, output;

	err = clamped_value(priv, feature_id, buf, &value);
	if (err)
		return err;

	mutex_lock(&priv->fancurve_mutex);
	err = wmi_other_method_set_value(feature_id, value, &output);
	mutex_unlock(&priv->fancurve_mutex);

	if (err)
		return -EINVAL;

	return count;
}

/*
 * Like wmi_common_method_other_store(), but for a boolean: parsed with
 * kstrtobool, so no capability-data clamping is needed. Keep the two in
 * step if the common store changes.
 */
static ssize_t instant_boot_store(struct device *dev, const char *buf,
				  size_t count,
				  enum OtherMethodFeature feature_id)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool enable;
	int err, output;

	if (kstrtobool(buf, &enable))
		return -EINVAL;

	mutex_lock(&priv->fancurve_mutex);
	err = wmi_other_method_set_value(feature_id, enable, &output);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EINVAL;

	return count;
}

static ssize_t instant_boot_ac_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	return wmi_common_method_other_show(dev_get_drvdata(dev), buf,
					    OtherMethodFeature_INSTANT_BOOT_AC);
}

static ssize_t instant_boot_ac_store(struct device *dev,
				     struct device_attribute *attr,
				     const char *buf, size_t count)
{
	return instant_boot_store(dev, buf, count,
				  OtherMethodFeature_INSTANT_BOOT_AC);
}

static DEVICE_ATTR_RW(instant_boot_ac);

static ssize_t instant_boot_usb_pd_show(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	return wmi_common_method_other_show(
		dev_get_drvdata(dev), buf,
		OtherMethodFeature_INSTANT_BOOT_USB_PD);
}

static ssize_t instant_boot_usb_pd_store(struct device *dev,
					 struct device_attribute *attr,
					 const char *buf, size_t count)
{
	return instant_boot_store(dev, buf, count,
				  OtherMethodFeature_INSTANT_BOOT_USB_PD);
}

static DEVICE_ATTR_RW(instant_boot_usb_pd);

static ssize_t cpu_shortterm_powerlimit_show(struct device *dev,
					     struct device_attribute *attr,
					     char *buf)
{
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf,
			OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT);
	default:
		err = show_simple_wmi_attribute_from_buffer(
			dev, attr, buf, WMI_GUID_LENOVO_CPU_METHOD, 0,
			WMI_METHOD_ID_CPU_GET_SHORTTERM_POWERLIMIT, 16, 0, 1);
	}
	return err;
}

static ssize_t cpu_shortterm_powerlimit_store(struct device *dev,
					      struct device_attribute *attr,
					      const char *buf, size_t count)
{
	int value, err;
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3) {
		/*
		 * PL1/PL2 coupling: keep the long-term limit at or below
		 * the short-term one (only models with has_pl_coupling).
		 * Both writes go through wmi_common_method_other_store(),
		 * which clamps against the capability data.
		 */
		if (priv->cpu_pl_coupling) {
			int pl1;

			err = clamped_value(
				priv,
				OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT,
				buf, &value);
			if (err)
				return err;

			if (!wmi_other_method_get_value(
				    OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT,
				    &pl1) &&
			    pl1 > value)
				wmi_common_method_other_store(
					priv, buf, count,
					OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT);
		}

		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT);
	}

	return store_simple_wmi_attribute(
		dev, attr, buf, count, WMI_GUID_LENOVO_CPU_METHOD, 0,
		WMI_METHOD_ID_CPU_SET_SHORTTERM_POWERLIMIT, false, 1);
}

static DEVICE_ATTR_RW(cpu_shortterm_powerlimit);

static ssize_t cpu_longterm_powerlimit_show(struct device *dev,
					    struct device_attribute *attr,
					    char *buf)
{
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf,
			OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT);
	default:
		err = show_simple_wmi_attribute_from_buffer(
			dev, attr, buf, WMI_GUID_LENOVO_CPU_METHOD, 0,
			WMI_METHOD_ID_CPU_GET_LONGTERM_POWERLIMIT, 16, 0, 1);
	}
	return err;
}

static ssize_t cpu_longterm_powerlimit_store(struct device *dev,
					     struct device_attribute *attr,
					     const char *buf, size_t count)
{
	int value, err;
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3) {
		/*
		 * PL1/PL2 coupling: keep the short-term limit at or above
		 * the long-term one (only models with has_pl_coupling).
		 * Both writes go through wmi_common_method_other_store(),
		 * which clamps against the capability data.
		 */
		if (priv->cpu_pl_coupling) {
			int pl2;

			err = clamped_value(
				priv,
				OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT,
				buf, &value);
			if (err)
				return err;

			if (!wmi_other_method_get_value(
				    OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT,
				    &pl2) &&
			    value > pl2)
				wmi_common_method_other_store(
					priv, buf, count,
					OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT);
		}

		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT);
	}

	return store_simple_wmi_attribute(
		dev, attr, buf, count, WMI_GUID_LENOVO_CPU_METHOD, 0,
		WMI_METHOD_ID_CPU_SET_LONGTERM_POWERLIMIT, false, 1);
}

static DEVICE_ATTR_RW(cpu_longterm_powerlimit);

static ssize_t cpu_default_powerlimit_show(struct device *dev,
					   struct device_attribute *attr,
					   char *buf)
{
	return show_simple_wmi_attribute(
		dev, attr, buf, WMI_GUID_LENOVO_CPU_METHOD, 0,
		WMI_METHOD_ID_CPU_GET_DEFAULT_POWERLIMIT, false, 1);
}

static DEVICE_ATTR_RO(cpu_default_powerlimit);

static ssize_t cpu_pl_coupling_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	return sysfs_emit(buf, "%d\n", priv->cpu_pl_coupling);
}

static ssize_t cpu_pl_coupling_store(struct device *dev,
				     struct device_attribute *attr,
				     const char *buf, size_t count)
{
	int val, err;
	struct legion_private *priv = dev_get_drvdata(dev);

	err = kstrtoint(buf, 0, &val);
	if (err)
		return err;

	priv->cpu_pl_coupling = !!val;
	return count;
}

static DEVICE_ATTR_RW(cpu_pl_coupling);

static ssize_t cpu_peak_powerlimit_show(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_CPU_PEAK_POWER_LIMIT);
	default:
		return show_simple_wmi_attribute(
			dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
			WMI_METHOD_ID_CPU_GET_PEAK_POWERLIMIT, false, 1);
	}
}

static ssize_t cpu_peak_powerlimit_store(struct device *dev,
					 struct device_attribute *attr,
					 const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_CPU_PEAK_POWER_LIMIT);

	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_GPU_METHOD, 0,
					  WMI_METHOD_ID_CPU_SET_PEAK_POWERLIMIT,
					  false, 1);
}

static DEVICE_ATTR_RW(cpu_peak_powerlimit);

static ssize_t cpu_apu_sppt_powerlimit_show(struct device *dev,
					    struct device_attribute *attr,
					    char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_APU_PPT_POWER_LIMIT);
	default:
		return show_simple_wmi_attribute(
			dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
			WMI_METHOD_ID_CPU_GET_APU_SPPT_POWERLIMIT, false, 1);
	}
}

static ssize_t cpu_apu_sppt_powerlimit_store(struct device *dev,
					     struct device_attribute *attr,
					     const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_APU_PPT_POWER_LIMIT);

	return store_simple_wmi_attribute(
		dev, attr, buf, count, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_CPU_SET_APU_SPPT_POWERLIMIT, false, 1);
}

static DEVICE_ATTR_RW(cpu_apu_sppt_powerlimit);

static ssize_t cpu_cross_loading_powerlimit_show(struct device *dev,
						 struct device_attribute *attr,
						 char *buf)
{
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf,
			OtherMethodFeature_CPU_CROSS_LOAD_POWER_LIMIT);
	default:
		err = show_simple_wmi_attribute(
			dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
			WMI_METHOD_ID_CPU_GET_CROSS_LOADING_POWERLIMIT, false,
			1);
	}
	return err;
}

static ssize_t cpu_cross_loading_powerlimit_store(struct device *dev,
						  struct device_attribute *attr,
						  const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_CPU_CROSS_LOAD_POWER_LIMIT);

	return store_simple_wmi_attribute(
		dev, attr, buf, count, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_CPU_SET_CROSS_LOADING_POWERLIMIT, false, 1);
}

static DEVICE_ATTR_RW(cpu_cross_loading_powerlimit);

static ssize_t gpu_oc_show(struct device *dev, struct device_attribute *attr,
			   char *buf)
{
	/*
	 * The Other Method GUID has no GPU OC feature; on models using it
	 * for power limits the boost budget is gpu_ppab_powerlimit instead
	 * (0x0201), and gpu_oc is hidden there (legion_attribute_uses_gpu_wmi).
	 */
	return show_simple_wmi_attribute(dev, attr, buf,
					 WMI_GUID_LENOVO_GPU_METHOD, 0,
					 WMI_METHOD_ID_GPU_GET_OC_STATUS, false,
					 1);
}

static ssize_t gpu_oc_store(struct device *dev, struct device_attribute *attr,
			    const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_GPU_METHOD, 0,
					  WMI_METHOD_ID_GPU_SET_OC_STATUS,
					  false, 1);
}

static DEVICE_ATTR_RW(gpu_oc);

static ssize_t gpu_ppab_powerlimit_show(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_GPU_POWER_BOOST);

	return show_simple_wmi_attribute_from_buffer(
		dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_GPU_GET_PPAB_POWERLIMIT, 16, 0, 1);
}

static ssize_t gpu_ppab_powerlimit_store(struct device *dev,
					 struct device_attribute *attr,
					 const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count, OtherMethodFeature_GPU_POWER_BOOST);

	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_GPU_METHOD, 0,
					  WMI_METHOD_ID_GPU_SET_PPAB_POWERLIMIT,
					  false, 1);
}

static DEVICE_ATTR_RW(gpu_ppab_powerlimit);

static ssize_t gpu_ctgp_powerlimit_show(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_GPU_cTGP);
	default:
		err = show_simple_wmi_attribute_from_buffer(
			dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
			WMI_METHOD_ID_GPU_GET_CTGP_POWERLIMIT, 16, 0, 1);
	}
	return err;
}

static ssize_t gpu_ctgp_powerlimit_store(struct device *dev,
					 struct device_attribute *attr,
					 const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count, OtherMethodFeature_GPU_cTGP);

	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_GPU_METHOD, 0,
					  WMI_METHOD_ID_GPU_SET_CTGP_POWERLIMIT,
					  false, 1);
}

static DEVICE_ATTR_RW(gpu_ctgp_powerlimit);

static ssize_t gpu_ctgp2_powerlimit_show(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	return show_simple_wmi_attribute_from_buffer(
		dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_GPU_GET_CTGP_POWERLIMIT, 16, 0x0C, 1);
}

static DEVICE_ATTR_RO(gpu_ctgp2_powerlimit);

// TOOD: probably remove again because provided by other means; only useful for overclocking
static ssize_t
gpu_default_ppab_ctrgp_powerlimit_show(struct device *dev,
				       struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(
		dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_GPU_GET_DEFAULT_PPAB_CTGP_POWERLIMIT, false, 1);
}
static DEVICE_ATTR_RO(gpu_default_ppab_ctrgp_powerlimit);

static ssize_t gpu_temperature_limit_show(struct device *dev,
					  struct device_attribute *attr,
					  char *buf)
{
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_GPU_TEMPERATURE_LIMIT);
	default:
		err = show_simple_wmi_attribute(
			dev, attr, buf, WMI_GUID_LENOVO_GPU_METHOD, 0,
			WMI_METHOD_ID_GPU_GET_TEMPERATURE_LIMIT, false, 1);
	}
	return err;
}

static ssize_t gpu_temperature_limit_store(struct device *dev,
					   struct device_attribute *attr,
					   const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_GPU_TEMPERATURE_LIMIT);

	return store_simple_wmi_attribute(
		dev, attr, buf, count, WMI_GUID_LENOVO_GPU_METHOD, 0,
		WMI_METHOD_ID_GPU_SET_TEMPERATURE_LIMIT, false, 1);
}

static ssize_t cpu_temperature_limit_show(struct device *dev,
					  struct device_attribute *attr,
					  char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_CPU_TEMPERATURE_LIMIT);
	default:
		return -EINVAL;
	}
}

static ssize_t cpu_temperature_limit_store(struct device *dev,
					   struct device_attribute *attr,
					   const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_CPU_TEMPERATURE_LIMIT);

	return -EINVAL;
}

static ssize_t cpu_l1_tau_show(struct device *dev,
			       struct device_attribute *attr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf, OtherMethodFeature_CPU_L1_TAU);
	default:
		return -EINVAL;
	}
}

static ssize_t cpu_l1_tau_store(struct device *dev,
				struct device_attribute *attr, const char *buf,
				size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count, OtherMethodFeature_CPU_L1_TAU);

	return -EINVAL;
}

static ssize_t gpu_power_target_offset_show(struct device *dev,
					    struct device_attribute *attr,
					    char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	switch (priv->conf->access_method_powerlimits) {
	case ACCESS_METHOD_WMI3:
		return wmi_common_method_other_show(
			priv, buf,
			OtherMethodFeature_GPU_POWER_TARGET_ON_AC_OFFSET_FROM_BASELINE);
	default:
		return -EINVAL;
	}
}

static ssize_t gpu_power_target_offset_store(struct device *dev,
					     struct device_attribute *attr,
					     const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);

	if (priv->conf->access_method_powerlimits == ACCESS_METHOD_WMI3)
		return wmi_common_method_other_store(
			priv, buf, count,
			OtherMethodFeature_GPU_POWER_TARGET_ON_AC_OFFSET_FROM_BASELINE);

	return -EINVAL;
}

static DEVICE_ATTR_RW(gpu_temperature_limit);
static DEVICE_ATTR_RW(cpu_temperature_limit);
static DEVICE_ATTR_RW(cpu_l1_tau);
static DEVICE_ATTR_RW(gpu_power_target_offset);

// TOOD: probably remove again because provided by other means; only useful for overclocking
static ssize_t gpu_boost_clock_show(struct device *dev,
				    struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 WMI_GUID_LENOVO_GPU_METHOD, 0,
					 WMI_METHOD_ID_GPU_GET_BOOST_CLOCK,
					 false, 1);
}
static DEVICE_ATTR_RO(gpu_boost_clock);

static ssize_t fan_fullspeed_show(struct device *dev,
				  struct device_attribute *attr, char *buf)
{
	bool state = false;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	mutex_lock(&priv->fancurve_mutex);
	err = read_fanfullspeed(priv, &state);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EINVAL;

	return sysfs_emit(buf, "%d\n", state);
}

static int fanfullspeed_write_allowed(struct legion_private *priv, bool state)
{
	int powermode;
	int err;

	if (!state || !priv->conf->fanfullspeed_requires_custom_powermode)
		return 0;

	err = read_fan_control_mode(priv, &powermode);
	if (err)
		return err;

	if (powermode != LEGION_WMI_POWERMODE_CUSTOM) {
		pr_info("fan_fullspeed needs powermode %d (custom), current is %d\n",
			LEGION_WMI_POWERMODE_CUSTOM, powermode);
		return -EBUSY;
	}

	return 0;
}

static ssize_t fan_fullspeed_store(struct device *dev,
				   struct device_attribute *attr,
				   const char *buf, size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	bool state;
	int err;

	err = kstrtobool(buf, &state);
	if (err)
		return err;

	mutex_lock(&priv->fancurve_mutex);
	err = fan_control_write_allowed(priv);
	if (!err)
		err = fanfullspeed_write_allowed(priv, state);
	if (!err)
		err = write_fanfullspeed(priv, state);
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return err;

	return count;
}

static DEVICE_ATTR_RW(fan_fullspeed);

static ssize_t fan_maxspeed_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	return show_simple_wmi_attribute(dev, attr, buf,
					 WMI_GUID_LENOVO_FAN_METHOD, 0,
					 WMI_METHOD_ID_FAN_GET_MAXSPEED, false,
					 1);
}

static ssize_t fan_maxspeed_store(struct device *dev,
				  struct device_attribute *attr,
				  const char *buf, size_t count)
{
	return store_simple_wmi_attribute(dev, attr, buf, count,
					  WMI_GUID_LENOVO_FAN_METHOD, 0,
					  WMI_METHOD_ID_FAN_SET_MAXSPEED, false,
					  1);
}

static DEVICE_ATTR_RW(fan_maxspeed);

static ssize_t powermode_show(struct device *dev, struct device_attribute *attr,
			      char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	int power_mode;

	mutex_lock(&priv->fancurve_mutex);
	sync_powermode_locked(priv);
	power_mode = priv->current_powermode;
	mutex_unlock(&priv->fancurve_mutex);
	return sysfs_emit(buf, "%d\n", power_mode);
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
static void legion_platform_profile_notify(struct device *dev);
#else
static void legion_platform_profile_notify(void);
#endif

static ssize_t powermode_store(struct device *dev,
			       struct device_attribute *attr, const char *buf,
			       size_t count)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	unsigned int powermode;
	int err;

	err = kstrtouint(buf, 0, &powermode);
	if (err)
		return err;

	switch (powermode) {
	case LEGION_WMI_POWERMODE_LOW_POWER:
	case LEGION_WMI_POWERMODE_BALANCED:
	case LEGION_WMI_POWERMODE_PERFORMANCE:
	case LEGION_WMI_POWERMODE_CUSTOM:
	case LEGION_WMI_POWERMODE_MAX_POWER:
		break;
	default:
		return -EINVAL;
	}

	mutex_lock(&priv->fancurve_mutex);
	err = write_powermode(priv, powermode);
	if (!err)
		priv->current_powermode = powermode;
	mutex_unlock(&priv->fancurve_mutex);
	if (err)
		return -EINVAL;

	// TODO: better?
	// we have to wait a bit before change is done in hardware and
	// readback done after notifying returns correct value, otherwise
	// the notified reader will read old value
	msleep(500);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	legion_platform_profile_notify(priv->ppdev);
#else
	legion_platform_profile_notify();
#endif

	return count;
}

static DEVICE_ATTR_RW(powermode);

static ssize_t fantable_ladder_show(const struct fantable_ladder *ladder,
				    char *buf)
{
	ssize_t count = 0;
	int i;

	for (i = 0; i < ladder->level_count; i++)
		count += sysfs_emit_at(buf, count, i ? " %u" : "%u",
				       ladder->rpms[i]);
	count += sysfs_emit_at(buf, count, "\n");

	return count;
}

static ssize_t fan1_level_rpm_table_show(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	ssize_t err;

	mutex_lock(&priv->fancurve_mutex);
	err = fantable_ensure(priv);
	if (!err) {
		if (priv->fantable_fan1_valid)
			err = fantable_ladder_show(&priv->fantable_fan1, buf);
		else
			err = -ENODATA;
	}
	mutex_unlock(&priv->fancurve_mutex);

	return err;
}

static ssize_t fan2_level_rpm_table_show(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	ssize_t err;

	mutex_lock(&priv->fancurve_mutex);
	err = fantable_ensure(priv);
	if (!err) {
		if (priv->fantable_fan2_valid)
			err = fantable_ladder_show(&priv->fantable_fan2, buf);
		else
			err = -ENODATA;
	}
	mutex_unlock(&priv->fancurve_mutex);

	return err;
}

static DEVICE_ATTR_RO(fan1_level_rpm_table);
static DEVICE_ATTR_RO(fan2_level_rpm_table);

/*
 * Firmware defaults of a CPU power limit per power mode, from
 * LENOVO_CAPABILITY_DATA_01 (feature id in bits 31..16, mode in bits
 * 15..8). Some firmware applies power limits itself only in custom mode
 * (Q7CN: the WMAE setters update the live EC limits only when ODV1 == 3)
 * and leaves the other modes to OEM software, so userspace (legiond) needs
 * these values to apply the right limits on a mode change.
 */
static const struct {
	u8 mode;
	const char *profile;
} powerlimit_default_modes[] = {
	{ LEGION_WMI_POWERMODE_LOW_POWER, "low-power" },
	{ LEGION_WMI_POWERMODE_BALANCED, "balanced" },
	{ LEGION_WMI_POWERMODE_PERFORMANCE, "performance" },
	{ LEGION_WMI_POWERMODE_MAX_POWER, "max-power" },
	{ LEGION_WMI_POWERMODE_CUSTOM, "custom" },
};

static bool powerlimit_defaults_available(const struct legion_private *priv,
					  enum OtherMethodFeature feature)
{
	size_t i;

	for (i = 0; i < ARRAY_SIZE(powerlimit_default_modes); i++) {
		if (capdata01_lookup(priv, (u32)feature >> 16,
				     powerlimit_default_modes[i].mode))
			return true;
	}
	return false;
}

/* "profile:watts" for every mode the firmware publishes a default for */
static ssize_t powerlimit_defaults_show(const struct legion_private *priv,
					char *buf,
					enum OtherMethodFeature feature)
{
	ssize_t count = 0;
	size_t i;

	for (i = 0; i < ARRAY_SIZE(powerlimit_default_modes); i++) {
		const struct capdata01 *cd =
			capdata01_lookup(priv, (u32)feature >> 16,
					 powerlimit_default_modes[i].mode);

		if (!cd)
			continue;
		count += sysfs_emit_at(buf, count, "%s%s:%u", count ? " " : "",
				       powerlimit_default_modes[i].profile,
				       cd->default_value);
	}
	if (!count)
		return -ENODATA;
	count += sysfs_emit_at(buf, count, "\n");

	return count;
}

static ssize_t
cpu_longterm_powerlimit_defaults_show(struct device *dev,
				      struct device_attribute *attr, char *buf)
{
	return powerlimit_defaults_show(
		dev_get_drvdata(dev), buf,
		OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT);
}

static ssize_t
cpu_shortterm_powerlimit_defaults_show(struct device *dev,
				       struct device_attribute *attr, char *buf)
{
	return powerlimit_defaults_show(
		dev_get_drvdata(dev), buf,
		OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT);
}

static DEVICE_ATTR_RO(cpu_longterm_powerlimit_defaults);
static DEVICE_ATTR_RO(cpu_shortterm_powerlimit_defaults);

static ssize_t fancurve_speed_unit_show(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	enum fan_speed_unit unit = FAN_SPEED_UNIT_RPM_HUNDRED;

	if (priv->conf->access_method_fancurve == ACCESS_METHOD_WMI3)
		unit = wmi_fancurve_speed_unit(priv->conf);
	if (unit == FAN_SPEED_UNIT_LEVEL)
		return sysfs_emit(buf, "level\n");
	if (unit == FAN_SPEED_UNIT_RPM_HUNDRED)
		return sysfs_emit(buf, "rpm\n");
	return sysfs_emit(buf, "percent\n");
}

static DEVICE_ATTR_RO(fancurve_speed_unit);

static struct attribute *legion_sysfs_attributes[] = {
	&dev_attr_fancurve_speed_unit.attr,
	&dev_attr_fan1_level_rpm_table.attr,
	&dev_attr_fan2_level_rpm_table.attr,
	&dev_attr_cpu_longterm_powerlimit_defaults.attr,
	&dev_attr_cpu_shortterm_powerlimit_defaults.attr,
	&dev_attr_powermode.attr,
	&dev_attr_lockfancontroller.attr,
	&dev_attr_fan_unlock.attr,
	&dev_attr_rapidcharge.attr,
	&dev_attr_battery_conservation.attr,
	&dev_attr_fn_lock.attr,
	&dev_attr_flip_to_start.attr,
	&dev_attr_instant_boot_ac.attr,
	&dev_attr_instant_boot_usb_pd.attr,
	&dev_attr_winkey.attr,
	&dev_attr_touchpad.attr,
	&dev_attr_gsync.attr,
	&dev_attr_powerchargemode.attr,
	&dev_attr_overdrive.attr,
	&dev_attr_cpumaxfrequency.attr,
	&dev_attr_isacfitforoc.attr,
	&dev_attr_cpu_oc.attr,
	&dev_attr_cpu_shortterm_powerlimit.attr,
	&dev_attr_cpu_longterm_powerlimit.attr,
	&dev_attr_cpu_apu_sppt_powerlimit.attr,
	&dev_attr_cpu_default_powerlimit.attr,
	&dev_attr_cpu_pl_coupling.attr,
	&dev_attr_cpu_peak_powerlimit.attr,
	&dev_attr_cpu_cross_loading_powerlimit.attr,
	&dev_attr_gpu_oc.attr,
	&dev_attr_gpu_ppab_powerlimit.attr,
	&dev_attr_gpu_ctgp_powerlimit.attr,
	&dev_attr_gpu_ctgp2_powerlimit.attr,
	&dev_attr_gpu_default_ppab_ctrgp_powerlimit.attr,
	&dev_attr_gpu_temperature_limit.attr,
	&dev_attr_cpu_temperature_limit.attr,
	&dev_attr_cpu_l1_tau.attr,
	&dev_attr_gpu_power_target_offset.attr,
	&dev_attr_gpu_boost_clock.attr,
	&dev_attr_fan_fullspeed.attr,
	&dev_attr_fan_maxspeed.attr,
	&dev_attr_thermalmode.attr,
	&dev_attr_issupportcpuoc.attr,
	&dev_attr_issupportgpuoc.attr,
	&dev_attr_aslcodeversion.attr,
	&dev_attr_igpumode.attr,
	&dev_attr_notify_dgpu.attr,
	&dev_attr_issupportigpumode.attr,
	&dev_attr_issupportgsync.attr,
	NULL
};

static bool legion_rapidcharge_is_supported(struct legion_private *priv)
{
	return acpi_method_exists(
		       priv->adev,
		       get_model_acpi_path(priv->conf,
					   ACPI_PATH_READ_RAPIDCHARGE)) &&
	       acpi_method_exists(
		       priv->adev,
		       get_model_acpi_path(priv->conf,
					   ACPI_PATH_WRITE_RAPIDCHARGE));
}

static bool legion_attribute_uses_cpu_wmi(const struct attribute *attr)
{
	return attr == &dev_attr_cpu_oc.attr ||
	       attr == &dev_attr_cpu_shortterm_powerlimit.attr ||
	       attr == &dev_attr_cpu_longterm_powerlimit.attr ||
	       attr == &dev_attr_cpu_default_powerlimit.attr;
}

static bool legion_attribute_uses_gpu_wmi(const struct attribute *attr)
{
	return attr == &dev_attr_cpu_apu_sppt_powerlimit.attr ||
	       attr == &dev_attr_cpu_peak_powerlimit.attr ||
	       attr == &dev_attr_cpu_cross_loading_powerlimit.attr ||
	       attr == &dev_attr_gpu_oc.attr ||
	       attr == &dev_attr_gpu_ppab_powerlimit.attr ||
	       attr == &dev_attr_gpu_ctgp_powerlimit.attr ||
	       attr == &dev_attr_gpu_ctgp2_powerlimit.attr ||
	       attr == &dev_attr_gpu_default_ppab_ctrgp_powerlimit.attr ||
	       attr == &dev_attr_gpu_temperature_limit.attr ||
	       attr == &dev_attr_gpu_boost_clock.attr;
}

static umode_t legion_sysfs_is_visible(struct kobject *kobj,
				       struct attribute *attr, int idx)
{
	struct device *dev = kobj_to_dev(kobj);
	struct legion_private *priv = dev_get_drvdata(dev);

	if (!priv)
		return attr->mode;

	if (attr == &dev_attr_rapidcharge.attr)
		return legion_rapidcharge_is_supported(priv) ? attr->mode : 0;
	if (attr == &dev_attr_battery_conservation.attr)
		return legion_rapidcharge_is_supported(priv) ? attr->mode : 0;
	if (attr == &dev_attr_overdrive.attr) {
		unsigned long supported;

		/*
		 * Hide it only when the firmware answers that the panel has no
		 * overdrive (Q7CN OLED: IsSupportOD checks PANT & 0x02 and the
		 * get/set methods then do nothing); keep it when the query
		 * fails, as before.
		 */
		if (!wmi_exec_noarg_int(LEGION_WMI_GAMEZONE_GUID, 0,
					WMI_METHOD_ID_ISSUPPORTOD,
					&supported) &&
		    supported == 0)
			return 0;
	}
	if (attr == &dev_attr_fn_lock.attr)
		return priv->conf->has_fn_lock ? attr->mode : 0;
	if (attr == &dev_attr_flip_to_start.attr)
		return priv->conf->has_flip_to_start ? attr->mode : 0;
	if (attr == &dev_attr_instant_boot_ac.attr ||
	    attr == &dev_attr_instant_boot_usb_pd.attr)
		return priv->conf->has_instant_boot ? attr->mode : 0;
	if (legion_attribute_uses_cpu_wmi(attr) &&
	    !wmi_has_guid(WMI_GUID_LENOVO_CPU_METHOD))
		return 0;
	if (legion_attribute_uses_gpu_wmi(attr) &&
	    !wmi_has_guid(WMI_GUID_LENOVO_GPU_METHOD))
		return 0;

	if (attr == &dev_attr_fan_fullspeed.attr &&
	    priv->conf->access_method_fanfullspeed == ACCESS_METHOD_NO_ACCESS)
		return 0;

	if (attr == &dev_attr_fancurve_speed_unit.attr &&
	    priv->conf->access_method_fancurve == ACCESS_METHOD_NO_ACCESS)
		return 0;

	if (attr == &dev_attr_cpu_longterm_powerlimit_defaults.attr)
		return powerlimit_defaults_available(
			       priv,
			       OtherMethodFeature_CPU_LONG_TERM_POWER_LIMIT) ?
			       attr->mode :
			       0;
	if (attr == &dev_attr_cpu_shortterm_powerlimit_defaults.attr)
		return powerlimit_defaults_available(
			       priv,
			       OtherMethodFeature_CPU_SHORT_TERM_POWER_LIMIT) ?
			       attr->mode :
			       0;

	if ((attr == &dev_attr_fan1_level_rpm_table.attr ||
	     attr == &dev_attr_fan2_level_rpm_table.attr) &&
	    !(priv->conf->access_method_fancurve == ACCESS_METHOD_WMI3 &&
	      wmi_fancurve_speed_unit(priv->conf) == FAN_SPEED_UNIT_LEVEL &&
	      wmi_has_guid(WMI_GUID_LENOVO_FANTABLE_DATA)))
		return 0;

	if (priv->conf->skip_oc_controls &&
	    (attr == &dev_attr_cpu_oc.attr || attr == &dev_attr_gpu_oc.attr ||
	     attr == &dev_attr_cpu_shortterm_powerlimit.attr ||
	     attr == &dev_attr_cpu_longterm_powerlimit.attr ||
	     attr == &dev_attr_cpu_peak_powerlimit.attr ||
	     attr == &dev_attr_cpu_default_powerlimit.attr ||
	     attr == &dev_attr_cpu_apu_sppt_powerlimit.attr ||
	     attr == &dev_attr_cpu_cross_loading_powerlimit.attr ||
	     attr == &dev_attr_gpu_ppab_powerlimit.attr ||
	     attr == &dev_attr_gpu_ctgp_powerlimit.attr ||
	     attr == &dev_attr_gpu_ctgp2_powerlimit.attr ||
	     attr == &dev_attr_gpu_default_ppab_ctrgp_powerlimit.attr ||
	     attr == &dev_attr_gpu_temperature_limit.attr ||
	     attr == &dev_attr_gpu_boost_clock.attr))
		return 0;

	if (attr == &dev_attr_cpu_pl_coupling.attr &&
	    !priv->conf->has_pl_coupling)
		return 0;

	if (attr == &dev_attr_fan_unlock.attr && !priv->conf->has_fan_unlock)
		return 0;

	if (attr == &dev_attr_fan_maxspeed.attr &&
	    priv->conf->skip_fan_maxspeed)
		return 0;

	if (attr == &dev_attr_lockfancontroller.attr &&
	    !lockfancontroller_supported(priv->conf))
		return 0;

	return attr->mode;
}

static const struct attribute_group legion_attribute_group = {
	.attrs = legion_sysfs_attributes,
	.is_visible = legion_sysfs_is_visible
};

static int legion_sysfs_init(struct legion_private *priv)
{
	return device_add_group(&priv->platform_device->dev,
				&legion_attribute_group);
}

static void legion_sysfs_exit(struct legion_private *priv)
{
	pr_info("Unloading legion sysfs\n");
	device_remove_group(&priv->platform_device->dev,
			    &legion_attribute_group);
	pr_info("Unloading legion sysfs done\n");
}

/* =============================  */
/* WMI + ACPI                     */
/* ============================   */
// heavily based on ideapad_laptop.c

// TODO: proper names if meaning of all events is clear
enum LEGION_WMI_EVENT {
	LEGION_WMI_EVENT_GAMEZONE = 1,
	LEGION_EVENT_A,
	LEGION_EVENT_B,
	LEGION_EVENT_C,
	LEGION_EVENT_D,
	LEGION_EVENT_E,
	LEGION_EVENT_F,
	LEGION_EVENT_G
};

struct legion_wmi_private {
	enum LEGION_WMI_EVENT event;
	/* GameZone method block: it has no notify id, so binding it never
	 * delivers events; this driver calls its methods by GUID either way.
	 */
	bool gamezone_method_block;
};

//static void legion_wmi_notify2(u32 value, void *context)
//    {
//	pr_info("WMI notify\n" );
//    }

static void legion_wmi_notify(struct wmi_device *wdev, union acpi_object *data)
{
	struct legion_wmi_private *wpriv;
	struct legion_private *priv;

	mutex_lock(&legion_shared_mutex);
	priv = legion_shared;
	if ((!priv) || (!priv->loaded)) {
		pr_info("Received WMI event while not initialized!\n");
		goto unlock;
	}

	wpriv = dev_get_drvdata(&wdev->dev);
	switch (wpriv->event) {
	case LEGION_EVENT_A:
		pr_info("Fan event: legion type: %d;  acpi type: %d (%d=integer)",
			wpriv->event, data->type, ACPI_TYPE_INTEGER);
		// TODO: here it is too early (first unlock mutext, then wait a bit)
		//legion_platform_profile_notify();
		break;
	default:
		pr_info("Event: legion type: %d;  acpi type: %d (%d=integer)",
			wpriv->event, data->type, ACPI_TYPE_INTEGER);
		break;
	}

unlock:
	mutex_unlock(&legion_shared_mutex);
	// todo; fix that!
	// problem: we get an event just before the powermode change (from the key?),
	// so if we notify too early, it will read the old power mode/platform profile
	msleep(500);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	if (priv)
		legion_platform_profile_notify(priv->ppdev);
#else
	legion_platform_profile_notify();
#endif
}

static int legion_wmi_probe(struct wmi_device *wdev, const void *context)
{
	const struct legion_wmi_private *ctx = context;
	struct legion_wmi_private *wpriv;
	bool leave_unbound = false;

	if (ctx->gamezone_method_block) {
		mutex_lock(&legion_shared_mutex);
		leave_unbound = legion_shared &&
				legion_shared->conf->leave_gamezone_wmi_unbound;
		mutex_unlock(&legion_shared_mutex);
	}
	if (leave_unbound) {
		dev_info(&wdev->dev,
			 "Leaving GameZone WMI to lenovo-wmi-gamezone\n");
		return -ENODEV;
	}

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

	*wpriv = *(const struct legion_wmi_private *)context;

	dev_set_drvdata(&wdev->dev, wpriv);
	dev_info(&wdev->dev, "Register after probing for WMI.\n");
	return 0;
}

static const struct legion_wmi_private legion_wmi_context_gamezone = {
	.event = LEGION_WMI_EVENT_GAMEZONE
};
static const struct legion_wmi_private legion_wmi_context_gamezone_methods = {
	.event = LEGION_WMI_EVENT_GAMEZONE,
	.gamezone_method_block = true
};
static const struct legion_wmi_private legion_wmi_context_a = {
	.event = LEGION_EVENT_A
};
static const struct legion_wmi_private legion_wmi_context_b = {
	.event = LEGION_EVENT_B
};
static const struct legion_wmi_private legion_wmi_context_c = {
	.event = LEGION_EVENT_C
};
static const struct legion_wmi_private legion_wmi_context_d = {
	.event = LEGION_EVENT_D
};
static const struct legion_wmi_private legion_wmi_context_e = {
	.event = LEGION_EVENT_E
};
static const struct legion_wmi_private legion_wmi_context_f = {
	.event = LEGION_EVENT_F
};

#define LEGION_WMI_GUID_FAN_EVENT "D320289E-8FEA-41E0-86F9-611D83151B5F"
#define LEGION_WMI_GUID_FAN2_EVENT "bc72a435-e8c1-4275-b3e2-d8b8074aba59"
#define LEGION_WMI_GUID_GAMEZONE_KEY_EVENT \
	"10afc6d9-ea8b-4590-a2e7-1cd3c84bb4b1"
#define LEGION_WMI_GUID_GAMEZONE_GPU_EVENT \
	"bfd42481-aee3-4502-a107-afb68425c5f8"
#define LEGION_WMI_GUID_GAMEZONE_OC_EVENT "d062906b-12d4-4510-999d-4831ee80e985"
#define LEGION_WMI_GUID_GAMEZONE_TEMP_EVENT \
	"bfd42481-aee3-4501-a107-afb68425c5f8"
//#define LEGION_WMI_GUID_GAMEZONE_DATA_EVENT  "887b54e3-dddc-4b2c-8b88-68a26a8835d0"

static const struct wmi_device_id legion_wmi_ids[] = {
	{ LEGION_WMI_GAMEZONE_GUID, &legion_wmi_context_gamezone_methods },
	{ LEGION_WMI_GUID_FAN_EVENT, &legion_wmi_context_a },
	{ LEGION_WMI_GUID_FAN2_EVENT, &legion_wmi_context_b },
	{ LEGION_WMI_GUID_GAMEZONE_KEY_EVENT, &legion_wmi_context_c },
	{ LEGION_WMI_GUID_GAMEZONE_GPU_EVENT, &legion_wmi_context_d },
	{ LEGION_WMI_GUID_GAMEZONE_OC_EVENT, &legion_wmi_context_e },
	{ LEGION_WMI_GUID_GAMEZONE_TEMP_EVENT, &legion_wmi_context_f },
	{ "8FC0DE0C-B4E4-43FD-B0F3-8871711C1294",
	  &legion_wmi_context_gamezone }, /* Legion 5 */
	{},
};
MODULE_DEVICE_TABLE(wmi, legion_wmi_ids);

static struct wmi_driver legion_wmi_driver = {
	.driver = {
		.name = "legion_wmi",
	},
	.id_table = legion_wmi_ids,
	.probe = legion_wmi_probe,
	.notify = legion_wmi_notify,
};

//acpi_status status = wmi_install_notify_handler(LEGION_WMI_GAMEZONE_GUID,
//				legion_wmi_notify2, NULL);
//if (ACPI_FAILURE(status)) {
//    return -ENODEV;
//}
//return 0;

static int legion_wmi_init(void)
{
	return wmi_driver_register(&legion_wmi_driver);
}

static void legion_wmi_exit(void)
{
	// TODO: remove this
	pr_info("Unloading legion WMI\n");

	//wmi_remove_notify_handler(LEGION_WMI_GAMEZONE_GUID);
	wmi_driver_unregister(&legion_wmi_driver);
	pr_info("Unloading legion WMI done\n");
}

/* =============================  */
/* Platform profile               */
/* ============================   */

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
static void legion_platform_profile_notify(struct device *dev)
#else
static void legion_platform_profile_notify(void)
#endif
{
	if (!enable_platformprofile) {
		pr_info("Skipping platform_profile_notify because enable_platformprofile is false\n");
		return;
	}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	platform_profile_notify(dev);
#else
	platform_profile_notify();
#endif
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
static int legion_platform_profile_get(struct device *dev,
				       enum platform_profile_option *profile)
#else
static int legion_platform_profile_get(struct platform_profile_handler *pprof,
				       enum platform_profile_option *profile)
#endif
{
	int powermode;
	struct legion_private *priv;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	priv = dev_get_drvdata(dev);
#else
	priv = container_of(pprof, struct legion_private,
			    platform_profile_handler);
#endif
	read_powermode(priv, &powermode);

	switch (powermode) {
	case LEGION_WMI_POWERMODE_BALANCED:
		*profile = PLATFORM_PROFILE_BALANCED;
		break;
	case LEGION_WMI_POWERMODE_PERFORMANCE:
		*profile = PLATFORM_PROFILE_PERFORMANCE;
		break;
	case LEGION_WMI_POWERMODE_LOW_POWER:
		*profile = PLATFORM_PROFILE_LOW_POWER;
		break;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	case LEGION_WMI_POWERMODE_CUSTOM:
		*profile = PLATFORM_PROFILE_CUSTOM;
		break;
#endif
	case LEGION_WMI_POWERMODE_MAX_POWER:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 19, 0)
		*profile = PLATFORM_PROFILE_MAX_POWER;
#else
		*profile = PLATFORM_PROFILE_PERFORMANCE;
#endif
		break;
	default:
		return -EINVAL;
	}
	return 0;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
static int legion_platform_profile_set(struct device *dev,
				       enum platform_profile_option profile)
#else
static int legion_platform_profile_set(struct platform_profile_handler *pprof,
				       enum platform_profile_option profile)
#endif
{
	int powermode;
	struct legion_private *priv;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	priv = dev_get_drvdata(dev);
#else
	priv = container_of(pprof, struct legion_private,
			    platform_profile_handler);
#endif
	switch (profile) {
	case PLATFORM_PROFILE_BALANCED:
		powermode = LEGION_WMI_POWERMODE_BALANCED;
		break;
	case PLATFORM_PROFILE_PERFORMANCE:
		powermode = LEGION_WMI_POWERMODE_PERFORMANCE;
		break;
	case PLATFORM_PROFILE_LOW_POWER:
		powermode = LEGION_WMI_POWERMODE_LOW_POWER;
		break;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	case PLATFORM_PROFILE_CUSTOM:
		powermode = LEGION_WMI_POWERMODE_CUSTOM;
		break;
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 19, 0)
	case PLATFORM_PROFILE_MAX_POWER:
		powermode = LEGION_WMI_POWERMODE_MAX_POWER;
		break;
#endif
	default:
		return -EOPNOTSUPP;
	}

	return write_powermode(priv, powermode);
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
static bool conf_has_custom_powermode;
static bool conf_has_extreme_powermode;
static enum access_method conf_access_method_powermode;

static int legion_platform_profile_probe(void *drvdata, unsigned long *choices)
{
	set_bit(PLATFORM_PROFILE_LOW_POWER, choices);
	set_bit(PLATFORM_PROFILE_BALANCED, choices);
	set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	if (conf_has_custom_powermode &&
	    conf_access_method_powermode == ACCESS_METHOD_WMI)
		set_bit(PLATFORM_PROFILE_CUSTOM, choices);
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 19, 0)
	if (conf_has_extreme_powermode &&
	    conf_access_method_powermode == ACCESS_METHOD_WMI)
		set_bit(PLATFORM_PROFILE_MAX_POWER, choices);
#endif
	return 0;
}

static const struct platform_profile_ops legion_platform_profile_ops = {
	.probe = legion_platform_profile_probe,
	.profile_get = legion_platform_profile_get,
	.profile_set = legion_platform_profile_set,
};
#endif

static int legion_platform_profile_init(struct legion_private *priv)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	struct device *dev = &priv->platform_device->dev;
#else
	int err;
#endif

	if (!enable_platformprofile) {
		pr_info("Skipping creating platform profile support because enable_platformprofile is false\n");
		return 0;
	}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	conf_has_custom_powermode = priv->conf->has_custom_powermode;
	conf_has_extreme_powermode = priv->conf->has_extreme_powermode;
	conf_access_method_powermode = priv->conf->access_method_powermode;
#else
	priv->platform_profile_handler.profile_get =
		legion_platform_profile_get;
	priv->platform_profile_handler.profile_set =
		legion_platform_profile_set;

	set_bit(PLATFORM_PROFILE_LOW_POWER,
		priv->platform_profile_handler.choices);
	set_bit(PLATFORM_PROFILE_BALANCED,
		priv->platform_profile_handler.choices);
	set_bit(PLATFORM_PROFILE_PERFORMANCE,
		priv->platform_profile_handler.choices);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	if (priv->conf->has_custom_powermode &&
	    priv->conf->access_method_powermode == ACCESS_METHOD_WMI) {
		set_bit(PLATFORM_PROFILE_CUSTOM,
			priv->platform_profile_handler.choices);
	}
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 19, 0)
	if (priv->conf->has_extreme_powermode &&
	    priv->conf->access_method_powermode == ACCESS_METHOD_WMI) {
		set_bit(PLATFORM_PROFILE_MAX_POWER,
			priv->platform_profile_handler.choices);
	}
#endif
#endif

#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 14, 0)
	priv->ppdev = devm_platform_profile_register(
		dev, "lenovo-legion", priv, &legion_platform_profile_ops);
	if (IS_ERR(priv->ppdev))
		return PTR_ERR(priv->ppdev);
#else
	err = platform_profile_register(&priv->platform_profile_handler);
	if (err)
		return err;
#endif

	return 0;
}

static void legion_platform_profile_exit(struct legion_private *priv)
{
	if (!enable_platformprofile) {
		pr_info("Skipping unloading platform profile support because enable_platformprofile is false\n");
		return;
	}
	pr_info("Unloading legion platform profile\n");
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 14, 0)
	platform_profile_remove();
#endif
	pr_info("Unloading legion platform profile done\n");
}

/* =============================  */
/* hwom interface              */
/* ============================   */

// hw-mon interface

// todo: register_group or register_info?

// TODO: use one common function (like here) or one function per attribute?
static ssize_t sensor_label_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	int sensor_id = (to_sensor_dev_attr(attr))->index;
	const char *label;

	switch (sensor_id) {
	case SENSOR_CPU_TEMP_ID:
		label = "CPU Temperature";
		break;
	case SENSOR_GPU_TEMP_ID:
		label = "GPU Temperature";
		break;
	case SENSOR_IC_TEMP_ID:
		label = "IC Temperature";
		break;
	case SENSOR_FAN1_RPM_ID:
		label = "Fan 1";
		break;
	case SENSOR_FAN2_RPM_ID:
		label = "Fan 2";
		break;
	case SENSOR_FAN3_RPM_ID:
		label = "Fan 3";
		break;
	case SENSOR_FAN4_RPM_ID:
		label = "Fan 4";
		break;
	case SENSOR_FAN1_TARGET_RPM_ID:
		label = "Fan 1 Target";
		break;
	case SENSOR_FAN2_TARGET_RPM_ID:
		label = "Fan 2 Target";
		break;
	default:
		return -EOPNOTSUPP;
	}

	return sysfs_emit(buf, "%s\n", label);
}

// TODO: use one common function (like here) or one function per attribute?
static ssize_t sensor_show(struct device *dev, struct device_attribute *devattr,
			   char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	int sensor_id = (to_sensor_dev_attr(devattr))->index;
	struct sensor_values values;
	int outval;
	int err = -EIO;

	switch (sensor_id) {
	case SENSOR_CPU_TEMP_ID:
		err = read_temperature(priv, 0, &outval);
		if (!err)
			outval *= 1000;
		break;
	case SENSOR_GPU_TEMP_ID:
		err = read_temperature(priv, 1, &outval);
		if (!err)
			outval *= 1000;
		break;
	case SENSOR_IC_TEMP_ID:
		ec_read_sensor_values(&priv->ecram, priv->conf, &values);
		outval = 1000 * values.ic_temp_celsius;
		err = 0;
		break;
	case SENSOR_FAN1_RPM_ID:
		err = read_fanspeed(priv, 0, &outval);
		break;
	case SENSOR_FAN2_RPM_ID:
		err = read_fanspeed(priv, 1, &outval);
		break;
	case SENSOR_FAN3_RPM_ID:
		err = read_fanspeed(priv, 2, &outval);
		break;
	case SENSOR_FAN4_RPM_ID:
		err = read_fanspeed(priv, 3, &outval);
		break;
	case SENSOR_FAN1_TARGET_RPM_ID:
		ec_read_sensor_values(&priv->ecram, priv->conf, &values);
		outval = values.fan1_target_rpm;
		err = 0;
		break;
	case SENSOR_FAN2_TARGET_RPM_ID:
		ec_read_sensor_values(&priv->ecram, priv->conf, &values);
		outval = values.fan2_target_rpm;
		err = 0;
		break;
	default:
		pr_info("Error reading sensor value with id %d\n", sensor_id);
		return -EOPNOTSUPP;
	}
	if (err)
		return err;

	return sysfs_emit(buf, "%d\n", outval);
}

static SENSOR_DEVICE_ATTR_RO(temp1_input, sensor, SENSOR_CPU_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(temp1_label, sensor_label, SENSOR_CPU_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(temp2_input, sensor, SENSOR_GPU_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(temp2_label, sensor_label, SENSOR_GPU_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(temp3_input, sensor, SENSOR_IC_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(temp3_label, sensor_label, SENSOR_IC_TEMP_ID);
static SENSOR_DEVICE_ATTR_RO(fan1_input, sensor, SENSOR_FAN1_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan1_label, sensor_label, SENSOR_FAN1_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan2_input, sensor, SENSOR_FAN2_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan2_label, sensor_label, SENSOR_FAN2_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan3_input, sensor, SENSOR_FAN3_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan3_label, sensor_label, SENSOR_FAN3_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan4_input, sensor, SENSOR_FAN4_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan4_label, sensor_label, SENSOR_FAN4_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan1_target, sensor, SENSOR_FAN1_TARGET_RPM_ID);
static SENSOR_DEVICE_ATTR_RO(fan2_target, sensor, SENSOR_FAN2_TARGET_RPM_ID);

static struct attribute *sensor_hwmon_attributes[] = {
	&sensor_dev_attr_temp1_input.dev_attr.attr,
	&sensor_dev_attr_temp1_label.dev_attr.attr,
	&sensor_dev_attr_temp2_input.dev_attr.attr,
	&sensor_dev_attr_temp2_label.dev_attr.attr,
	&sensor_dev_attr_temp3_input.dev_attr.attr,
	&sensor_dev_attr_temp3_label.dev_attr.attr,
	&sensor_dev_attr_fan1_input.dev_attr.attr,
	&sensor_dev_attr_fan1_label.dev_attr.attr,
	&sensor_dev_attr_fan2_input.dev_attr.attr,
	&sensor_dev_attr_fan2_label.dev_attr.attr,
	&sensor_dev_attr_fan3_input.dev_attr.attr,
	&sensor_dev_attr_fan3_label.dev_attr.attr,
	&sensor_dev_attr_fan4_input.dev_attr.attr,
	&sensor_dev_attr_fan4_label.dev_attr.attr,
	&sensor_dev_attr_fan1_target.dev_attr.attr,
	&sensor_dev_attr_fan2_target.dev_attr.attr,
	NULL
};

static ssize_t fan_max_show(struct device *dev,
			    struct device_attribute *devattr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	int channel = to_sensor_dev_attr(devattr)->index;
	const struct fantable_ladder *ladder;

	/* Prefer the firmware's per-level fan table when the model has it:
	 * the row's current_fan_max_speed is the fan's real maximum.
	 */
	ladder = channel == 1 ? &priv->fantable_fan2 : &priv->fantable_fan1;
	if (priv->conf->has_fancurve_defaults &&
	    priv->conf->access_method_fancurve == ACCESS_METHOD_WMI3) {
		mutex_lock(&priv->fancurve_mutex);
		if (!fantable_ensure(priv) &&
		    (channel == 1 ? priv->fantable_fan2_valid :
				    priv->fantable_fan1_valid)) {
			int max_rpm = ladder->max_rpm;

			mutex_unlock(&priv->fancurve_mutex);
			return sysfs_emit(buf, "%d\n", max_rpm);
		}
		mutex_unlock(&priv->fancurve_mutex);
	}

	if (priv && priv->conf->fan_max_rpm)
		return sysfs_emit(buf, "%d\n", priv->conf->fan_max_rpm);

	return sysfs_emit(buf, "%d\n", MAX_RPM);
}

static ssize_t autopoint_show(struct device *dev,
			      struct device_attribute *devattr, char *buf)
{
	struct fancurve fancurve;
	int err;
	int value;
	struct legion_private *priv = dev_get_drvdata(dev);
	int fancurve_attr_id = to_sensor_dev_attr_2(devattr)->nr;
	int point_id = to_sensor_dev_attr_2(devattr)->index;
	bool ok = true;

	mutex_lock(&priv->fancurve_mutex);
	err = read_fancurve(priv, &fancurve);
	mutex_unlock(&priv->fancurve_mutex);

	if (err) {
		pr_info("Failed to read fancurve\n");
		return -EOPNOTSUPP;
	}
	if (!(point_id >= 0 && point_id < MAXFANCURVESIZE)) {
		pr_info("Failed to read fancurve due to wrong point id: %d\n",
			point_id);
		return -EOPNOTSUPP;
	}

	switch (fancurve_attr_id) {
	case FANCURVE_ATTR_PWM1:
		ok = fancurve_get_speed_pwm(&fancurve, point_id, 0, &value);
		break;
	case FANCURVE_ATTR_PWM2:
		ok = fancurve_get_speed_pwm(&fancurve, point_id, 1, &value);
		break;
	case FANCURVE_ATTR_CPU_TEMP:
		value = fancurve.points[point_id].cpu_max_temp_celsius;
		break;
	case FANCURVE_ATTR_CPU_HYST:
		value = fancurve.points[point_id].cpu_min_temp_celsius;
		break;
	case FANCURVE_ATTR_GPU_TEMP:
		value = fancurve.points[point_id].gpu_max_temp_celsius;
		break;
	case FANCURVE_ATTR_GPU_HYST:
		value = fancurve.points[point_id].gpu_min_temp_celsius;
		break;
	case FANCURVE_ATTR_IC_TEMP:
		value = fancurve.points[point_id].ic_max_temp_celsius;
		break;
	case FANCURVE_ATTR_IC_HYST:
		value = fancurve.points[point_id].ic_min_temp_celsius;
		break;
	case FANCURVE_ATTR_ACCEL:
		value = fancurve.points[point_id].accel;
		break;
	case FANCURVE_ATTR_DECEL:
		value = fancurve.points[point_id].decel;
		break;
	case FANCURVE_SIZE:
		value = fancurve.size;
		break;
	default:
		pr_info("Failed to read fancurve due to wrong attribute id: %d\n",
			fancurve_attr_id);
		return -EOPNOTSUPP;
	}
	if (!ok)
		value = 0;
	return sysfs_emit(buf, "%d\n", value);
}

static ssize_t autopoint_store(struct device *dev,
			       struct device_attribute *devattr,
			       const char *buf, size_t count)
{
	struct fancurve fancurve;
	int err;
	int value;
	bool valid;
	struct legion_private *priv = dev_get_drvdata(dev);
	int fancurve_attr_id = to_sensor_dev_attr_2(devattr)->nr;
	int point_id = to_sensor_dev_attr_2(devattr)->index;
	bool write_fancurve_size = false;

	if (!fancurve_attr_supported(priv->conf, fancurve_attr_id) ||
	    (fancurve_attr_id == FANCURVE_SIZE &&
	     priv->conf->access_method_fancurve != ACCESS_METHOD_EC))
		return -EOPNOTSUPP;

	if (!(point_id >= 0 && point_id < MAXFANCURVESIZE)) {
		pr_info("Failed to read fancurve due to wrong point id: %d\n",
			point_id);
		err = -EOPNOTSUPP;
		goto error;
	}

	err = kstrtoint(buf, 0, &value);
	if (err) {
		pr_info("Parsing hwmon store failed: error: %d; point_id: %d; fancurve_attr_id: %d\\n",
			err, point_id, fancurve_attr_id);
		goto error;
	}

	mutex_lock(&priv->fancurve_mutex);
	err = fan_control_write_allowed(priv);
	if (err)
		goto error_mutex;
	err = read_fancurve(priv, &fancurve);

	if (err) {
		pr_info("Failed to read fancurve\n");
		err = -EOPNOTSUPP;
		goto error_mutex;
	}

	switch (fancurve_attr_id) {
	case FANCURVE_ATTR_PWM1:
		valid = fancurve_set_speed_pwm(&fancurve, point_id, 0, value);
		break;
	case FANCURVE_ATTR_PWM2:
		valid = fancurve_set_speed_pwm(&fancurve, point_id, 1, value);
		break;
	case FANCURVE_ATTR_CPU_TEMP:
		valid = fancurve_set_cpu_temp_max(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_CPU_HYST:
		valid = fancurve_set_cpu_temp_min(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_GPU_TEMP:
		valid = fancurve_set_gpu_temp_max(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_GPU_HYST:
		valid = fancurve_set_gpu_temp_min(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_IC_TEMP:
		valid = fancurve_set_ic_temp_max(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_IC_HYST:
		valid = fancurve_set_ic_temp_min(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_ACCEL:
		valid = fancurve_set_accel(&fancurve, point_id, value);
		break;
	case FANCURVE_ATTR_DECEL:
		valid = fancurve_set_decel(&fancurve, point_id, value);
		break;
	case FANCURVE_SIZE:
		valid = fancurve_set_size(&fancurve, value, true);
		write_fancurve_size = true;
		break;
	default:
		pr_info("Failed to write fancurve due to wrong attribute id: %d\n",
			fancurve_attr_id);
		err = -EOPNOTSUPP;
		goto error_mutex;
	}

	if (!valid) {
		pr_info("Ignoring invalid fancurve value %d for attribute %d at point %d\n",
			value, fancurve_attr_id, point_id);
		err = -EOPNOTSUPP;
		goto error_mutex;
	}

	err = write_fancurve(priv, &fancurve, write_fancurve_size);
	if (err) {
		pr_info("Failed to write fancurve for accessing hwmon at point_id: %d\n",
			point_id);
		err = -EOPNOTSUPP;
		goto error_mutex;
	}

	mutex_unlock(&priv->fancurve_mutex);
	return count;

error_mutex:
	mutex_unlock(&priv->fancurve_mutex);
error:
	return err;
}

static ssize_t
fancurve_defaults_powermode_store(struct device *dev,
				  struct device_attribute *devattr,
				  const char *buf, size_t count)
{
	int value;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	err = kstrtoint(buf, 0, &value);
	if (err) {
		err = -1;
		pr_info("Parsing fancurve_defaults_powermode store failed: error:%d\n",
			err);
		goto error;
	}

	if (!(value == LEGION_WMI_POWERMODE_LOW_POWER ||
	      value == LEGION_WMI_POWERMODE_PERFORMANCE ||
	      value == LEGION_WMI_POWERMODE_BALANCED ||
	      value == LEGION_WMI_POWERMODE_CUSTOM ||
	      value == LEGION_WMI_POWERMODE_MAX_POWER)) {
		err = -1;
		pr_info("Parsing fancurve_defaults_powermode store failed invalid powermode id %d: error:%d\n",
			value, err);
		goto error;
	}

	mutex_lock(&priv->fancurve_mutex);
	err = wmi_write_fancurve_defaults(priv, value);
	if (err) {
		err = -1;
		pr_info("Failed to write auto points defaults\n");
		goto error_unlock;
	}
	fancurve_defaults_powermode = value;
	/*
	 * The firmware defaults replace the table, so the remembered user
	 * curve must not be re-applied over them after resume, nor returned
	 * by read_fancurve() as the cached curve if a later read fails.
	 */
	priv->resume_fancurve_valid = false;
	priv->fancurve_valid = false;
	mutex_unlock(&priv->fancurve_mutex);
	return count;

error_unlock:
	mutex_unlock(&priv->fancurve_mutex);
error:
	return err;
}

static ssize_t
fancurve_defaults_powermode_show(struct device *dev,
				 struct device_attribute *devattr, char *buf)
{
	struct legion_private *priv = dev_get_drvdata(dev);
	int power_mode;

	mutex_lock(&priv->fancurve_mutex);
	read_powermode(priv, &power_mode);
	mutex_unlock(&priv->fancurve_mutex);
	// set to 0 if not in CUSTOM mode (pressed Fn-Q)
	if (power_mode != LEGION_WMI_POWERMODE_CUSTOM)
		fancurve_defaults_powermode = 0;
	return sysfs_emit(buf, "%d\n", fancurve_defaults_powermode);
}

// pwm1
static SENSOR_DEVICE_ATTR_RO(fan1_max, fan_max, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point9_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point10_pwm, autopoint,
			       FANCURVE_ATTR_PWM1, 9);
// pwm2
static SENSOR_DEVICE_ATTR_RO(fan2_max, fan_max, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point7_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point8_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point9_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point10_pwm, autopoint,
			       FANCURVE_ATTR_PWM2, 9);
// CPU temp
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point9_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point10_temp, autopoint,
			       FANCURVE_ATTR_CPU_TEMP, 9);
// CPU temp hyst
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point9_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point10_temp_hyst, autopoint,
			       FANCURVE_ATTR_CPU_HYST, 9);
// GPU temp
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point7_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point8_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point9_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point10_temp, autopoint,
			       FANCURVE_ATTR_GPU_TEMP, 9);
// GPU temp hyst
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point1_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point2_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point3_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point4_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point5_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point6_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point7_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point8_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point9_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm2_auto_point10_temp_hyst, autopoint,
			       FANCURVE_ATTR_GPU_HYST, 9);
// IC temp
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point1_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point2_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point3_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point4_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point5_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point6_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point9_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point10_temp, autopoint,
			       FANCURVE_ATTR_IC_TEMP, 9);
// IC temp hyst
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point1_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point2_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point3_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point4_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point5_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point6_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point9_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point10_temp_hyst, autopoint,
			       FANCURVE_ATTR_IC_HYST, 9);
// accel
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point9_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point10_accel, autopoint,
			       FANCURVE_ATTR_ACCEL, 9);
// decel
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point1_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 0);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point2_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 1);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point3_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 2);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point4_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 3);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point5_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 4);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point6_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 5);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point7_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 6);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point8_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 7);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point9_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 8);
static SENSOR_DEVICE_ATTR_2_RW(pwm1_auto_point10_decel, autopoint,
			       FANCURVE_ATTR_DECEL, 9);
//size
static SENSOR_DEVICE_ATTR_2_RW(auto_points_size, autopoint, FANCURVE_SIZE, 0);
static SENSOR_DEVICE_ATTR_2_RW(fancurve_defaults_powermode,
			       fancurve_defaults_powermode, 0, 0);

static ssize_t minifancurve_show(struct device *dev,
				 struct device_attribute *devattr, char *buf)
{
	bool value;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	mutex_lock(&priv->fancurve_mutex);
	err = ec_read_minifancurve(&priv->ecram, priv->conf, &value);
	if (err) {
		pr_info("Failed to read minifancurve\n");
		goto error_unlock;
	}
	mutex_unlock(&priv->fancurve_mutex);
	return sysfs_emit(buf, "%d\n", value);

error_unlock:
	mutex_unlock(&priv->fancurve_mutex);
	return -EIO;
}

static ssize_t minifancurve_store(struct device *dev,
				  struct device_attribute *devattr,
				  const char *buf, size_t count)
{
	int value;
	int err;
	struct legion_private *priv = dev_get_drvdata(dev);

	err = kstrtoint(buf, 0, &value);
	if (err) {
		err = -1;
		pr_info("Parsing hwmon store failed: error:%d\n", err);
		goto error;
	}

	mutex_lock(&priv->fancurve_mutex);
	err = ec_write_minifancurve(&priv->ecram, priv->conf, value);
	if (err) {
		err = -1;
		pr_info("Failed to write minifancurve\n");
		goto error_unlock;
	}
	mutex_unlock(&priv->fancurve_mutex);
	return count;

error_unlock:
	mutex_unlock(&priv->fancurve_mutex);
error:
	return err;
}

static SENSOR_DEVICE_ATTR_RW(minifancurve, minifancurve, 0);

static struct attribute *fancurve_hwmon_attributes[] = {
	&sensor_dev_attr_fan1_max.dev_attr.attr,
	&sensor_dev_attr_fan2_max.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point1_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point2_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point3_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point4_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point5_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point6_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point7_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point8_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point9_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point10_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point1_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point2_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point3_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point4_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point5_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point6_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point7_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point8_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point9_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point10_pwm.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point1_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point2_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point3_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point4_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point5_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point6_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point7_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point8_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point9_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point10_temp.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point1_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point2_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point3_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point4_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point5_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point6_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point7_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point8_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point9_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point10_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point1_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point2_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point3_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point4_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point5_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point6_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point7_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point8_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point9_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point10_temp.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point1_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point2_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point3_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point4_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point5_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point6_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point7_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point8_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point9_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm2_auto_point10_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point1_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point2_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point3_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point4_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point5_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point6_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point7_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point8_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point9_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point10_temp.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point1_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point2_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point3_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point4_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point5_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point6_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point7_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point8_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point9_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm3_auto_point10_temp_hyst.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point1_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point2_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point3_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point4_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point5_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point6_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point7_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point8_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point9_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point10_accel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point1_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point2_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point3_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point4_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point5_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point6_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point7_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point8_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point9_decel.dev_attr.attr,
	&sensor_dev_attr_pwm1_auto_point10_decel.dev_attr.attr,
	//
	&sensor_dev_attr_auto_points_size.dev_attr.attr,
	&sensor_dev_attr_minifancurve.dev_attr.attr,
	&sensor_dev_attr_fancurve_defaults_powermode.dev_attr.attr, NULL
};

static umode_t legion_hwmon_sensor_is_visible(struct kobject *kobj,
					      struct attribute *attr, int idx)
{
	bool supported = true;
	struct device *dev = kobj_to_dev(kobj);
	struct legion_private *priv = dev_get_drvdata(dev);

	if (attr == &sensor_dev_attr_temp3_input.dev_attr.attr ||
	    attr == &sensor_dev_attr_temp3_label.dev_attr.attr)
		supported = supported && !priv->conf->skip_ic_temp;

	if (attr == &sensor_dev_attr_fan3_input.dev_attr.attr ||
	    attr == &sensor_dev_attr_fan3_label.dev_attr.attr)
		supported = supported && (priv->conf->has_four_fans ||
					  priv->conf->has_third_fan);

	if (attr == &sensor_dev_attr_fan4_input.dev_attr.attr ||
	    attr == &sensor_dev_attr_fan4_label.dev_attr.attr)
		supported = supported && priv->conf->has_four_fans;

	if (attr == &sensor_dev_attr_fan2_input.dev_attr.attr ||
	    attr == &sensor_dev_attr_fan2_label.dev_attr.attr ||
	    attr == &sensor_dev_attr_fan2_target.dev_attr.attr)
		supported = supported && !priv->conf->has_single_fan;

	return supported ? attr->mode : 0;
}

static umode_t legion_hwmon_fancurve_is_visible(struct kobject *kobj,
						struct attribute *attr, int idx)
{
	bool supported = true;
	struct device *dev = kobj_to_dev(kobj);
	struct legion_private *priv = dev_get_drvdata(dev);

	/* Retain the model-specific exclusions for auxiliary legacy controls. */
	if (priv->conf->wmi_fancurve_speed_only &&
	    (attr == &sensor_dev_attr_minifancurve.dev_attr.attr ||
	     attr == &sensor_dev_attr_fancurve_defaults_powermode.dev_attr.attr ||
	     attr == &sensor_dev_attr_fan2_max.dev_attr.attr))
		return 0;
	if (container_of(attr, struct device_attribute, attr)->show ==
	    autopoint_show) {
		struct device_attribute *devattr =
			container_of(attr, struct device_attribute, attr);
		int id = to_sensor_dev_attr_2(devattr)->nr;

		if (!fancurve_attr_supported(priv->conf, id))
			return 0;
		if (id == FANCURVE_SIZE &&
		    priv->conf->access_method_fancurve != ACCESS_METHOD_EC)
			return attr->mode & ~0222;
	}
	if (attr == &sensor_dev_attr_minifancurve.dev_attr.attr)
		supported = minifancurve_supported(priv->conf);
	if (attr == &sensor_dev_attr_fancurve_defaults_powermode.dev_attr.attr)
		supported = priv->conf->has_fancurve_defaults;

	supported = supported && (priv->conf->access_method_fancurve !=
				  ACCESS_METHOD_NO_ACCESS);

	return supported ? attr->mode : 0;
}

static const struct attribute_group legion_hwmon_sensor_group = {
	.attrs = sensor_hwmon_attributes,
	.is_visible = legion_hwmon_sensor_is_visible
};

static const struct attribute_group legion_hwmon_fancurve_group = {
	.attrs = fancurve_hwmon_attributes,
	.is_visible = legion_hwmon_fancurve_is_visible,
};

static const struct attribute_group *legion_hwmon_groups[] = {
	&legion_hwmon_sensor_group, &legion_hwmon_fancurve_group, NULL
};

static ssize_t legion_hwmon_init(struct legion_private *priv)
{
	//TODO: use hwmon_device_register_with_groups or
	// hwmon_device_register_with_info (latter means all hwmon functions have to be
	// changed)
	// some laptop driver do it in one way, some in the other
	// TODO: Use devm_hwmon_device_register_with_groups ?
	// some laptop drivers use this, some
	struct device *hwmon_dev = hwmon_device_register_with_groups(
		&priv->platform_device->dev, "legion_hwmon", priv,
		legion_hwmon_groups);
	if (IS_ERR_OR_NULL(hwmon_dev)) {
		pr_err("hwmon_device_register failed!\n");
		return PTR_ERR(hwmon_dev);
	}
	dev_set_drvdata(hwmon_dev, priv);
	priv->hwmon_dev = hwmon_dev;
	return 0;
}

static void legion_hwmon_exit(struct legion_private *priv)
{
	pr_info("Unloading legion hwon\n");
	if (priv->hwmon_dev) {
		hwmon_device_unregister(priv->hwmon_dev);
		priv->hwmon_dev = NULL;
	}
	pr_info("Unloading legion hwon done\n");
}

/* ACPI*/

static int acpi_init(struct legion_private *priv, struct acpi_device *adev)
{
	int err;
	unsigned long cfg;
	bool skip_acpi_sta_check;
	struct device *dev = &priv->platform_device->dev;
	const char *acpi_path;

	/*
	 * Ownership of priv->adev differs by kernel: before 7.0 it is the
	 * caller's ACPI companion reference (borrowed, never put here);
	 * on 7.0+ the virtual platform device has no companion, so the
	 * EC device looked up below is owned by the driver and put in
	 * acpi_exit(). acpi_dev_put() is only called on the owned case.
	 */
	priv->adev = adev;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
	/*
	 * The virtual platform device has no ACPI companion. The default
	 * ACPI paths are names relative to the embedded controller device
	 * (_STA, _CFG, VPC0.GBMD, ...), which is where the driver bound
	 * before kernel 7.0, so look that device up by its HID and keep
	 * evaluating relative to it. The reference is dropped in
	 * acpi_exit().
	 *
	 * Note: this only returns the first PNP0C09 device; Legion
	 * platforms declare a single embedded controller, but if a DSDT
	 * ever declared more than one, the first match might not be the
	 * right one -- absolute per-model paths are the fallback then.
	 */
	if (!priv->adev) {
		priv->adev = acpi_dev_get_first_match_dev("PNP0C09", NULL, -1);
		if (priv->adev)
			dev_info(dev, "Using ACPI device %s for EC methods\n",
				 dev_name(&priv->adev->dev));
	}
#endif
	if (!priv->adev)
		dev_info(dev, "No ACPI handle, will use FQN paths\n");
	skip_acpi_sta_check = force || (!priv->conf->acpi_check_dev);
	if (!skip_acpi_sta_check) {
		acpi_path = get_model_acpi_path(_model, ACPI_PATH_STA);
		err = eval_int(priv->adev, acpi_path, &cfg);
		if (err) {
			dev_info(dev, "Could not evaluate ACPI %s: %d\n",
				 acpi_path, err);
			goto err_acpi_init;
		}

		/*
		 * _CFG is only reported (here and in debugfs), never used to
		 * gate a feature, and not every Lenovo DSDT defines it on the
		 * embedded controller device, so its absence must not stop
		 * the probe; _STA above is the presence check.
		 */
		acpi_path = get_model_acpi_path(_model, ACPI_PATH_CFG);
		err = eval_int(priv->adev, acpi_path, &cfg);
		if (err)
			dev_info(dev, "Could not evaluate ACPI %s: %d\n",
				 acpi_path, err);
		else
			dev_info(dev, "ACPI CFG: %lu\n", cfg);
	} else {
		dev_info(dev, "Skipping ACPI _STA check");
	}

	return 0;

err_acpi_init:
	return err;
}

static void acpi_exit(struct legion_private *priv)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
	/* Reference taken in acpi_init(); acpi_dev_put() accepts NULL. */
	acpi_dev_put(priv->adev);
#endif
	priv->adev = NULL;
}

/* =============================  */
/* White Keyboard Backlight       */
/* ============================   */
// In style of ideapad-driver and with code modified from ideapad-driver.

static enum led_brightness
legion_kbd_bl_led_cdev_brightness_get(struct led_classdev *led_cdev)
{
	struct legion_private *priv =
		container_of(led_cdev, struct legion_private, kbd_bl.led);

	return legion_kbd_bl_brightness_get(priv);
}

static int legion_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
						 enum led_brightness brightness)
{
	struct legion_private *priv =
		container_of(led_cdev, struct legion_private, kbd_bl.led);

	return legion_kbd_bl_brightness_set(priv, brightness);
}

static int legion_kbd_bl_init(struct legion_private *priv)
{
	int brightness, err;

	if (WARN_ON(priv->kbd_bl.initialized)) {
		pr_info("Keyboard backlight already initialized\n");
		return -EEXIST;
	}

	if (priv->conf->access_method_keyboard == ACCESS_METHOD_NO_ACCESS) {
		pr_info("Keyboard backlight handling disabled by this driver\n");
		return -ENODEV;
	}

	brightness = legion_kbd_bl_brightness_get(priv);
	if (brightness < 0) {
		pr_info("Error reading keyboard brightness\n");
		return brightness;
	}

	priv->kbd_bl.last_brightness = brightness;

	// will be renamed to "platform::kbd_backlight_1" if it exists already
	priv->kbd_bl.led.name = "platform::" LED_FUNCTION_KBD_BACKLIGHT;
	priv->kbd_bl.led.max_brightness = 2;
	priv->kbd_bl.led.brightness_get = legion_kbd_bl_led_cdev_brightness_get;
	priv->kbd_bl.led.brightness_set_blocking =
		legion_kbd_bl_led_cdev_brightness_set;
	priv->kbd_bl.led.flags = LED_BRIGHT_HW_CHANGED;

	err = led_classdev_register(&priv->platform_device->dev,
				    &priv->kbd_bl.led);
	if (err)
		return err;

	priv->kbd_bl.initialized = true;

	return 0;
}

/**
 * Deinit keyboard backlight.
 *
 * Can also be called if init was not successful.
 *
 */
static void legion_kbd_bl_exit(struct legion_private *priv)
{
	if (!priv->kbd_bl.initialized)
		return;

	priv->kbd_bl.initialized = false;

	led_classdev_unregister(&priv->kbd_bl.led);
}

/* =============================  */
/* Additional light driver        */
/* ============================   */

static enum led_brightness
legion_wmi_cdev_brightness_get(struct led_classdev *led_cdev)
{
	struct legion_private *priv =
		container_of(led_cdev, struct legion_private, kbd_bl.led);
	struct light *light_ins = container_of(led_cdev, struct light, led);

	return legion_wmi_light_get(priv, light_ins->light_id,
				    light_ins->lower_limit,
				    light_ins->upper_limit);
}

static int legion_wmi_cdev_brightness_set(struct led_classdev *led_cdev,
					  enum led_brightness brightness)
{
	struct legion_private *priv =
		container_of(led_cdev, struct legion_private, kbd_bl.led);
	struct light *light_ins = container_of(led_cdev, struct light, led);

	return legion_wmi_light_set(priv, light_ins->light_id,
				    light_ins->lower_limit,
				    light_ins->upper_limit, brightness);
}

/* =============================  */
/* Y-Logo light via EC register   */
/* ============================   */
// EC register value: 0 = bright, 1 = dim, 2 = off. The sysfs LED
// brightness is mapped inversely so that 0 = off and 2 = bright.

static int legion_ec_ylogo_get(struct legion_private *priv)
{
	u8 value = ecram_read(&priv->ecram, priv->conf->ec_ylogo_register);

	if (value > 2)
		return -ERANGE;
	return 2 - value;
}

static int legion_ec_ylogo_set(struct legion_private *priv,
			       unsigned int brightness)
{
	if (brightness > 2)
		return -EINVAL;
	ecram_write(&priv->ecram, priv->conf->ec_ylogo_register,
		    2 - (u8)brightness);
	return 0;
}

static enum led_brightness
legion_ec_ylogo_cdev_brightness_get(struct led_classdev *led_cdev)
{
	struct light *light_ins = container_of(led_cdev, struct light, led);
	struct legion_private *priv =
		container_of(light_ins, struct legion_private, ylogo_light);

	return legion_ec_ylogo_get(priv);
}

static int legion_ec_ylogo_cdev_brightness_set(struct led_classdev *led_cdev,
					       enum led_brightness brightness)
{
	struct light *light_ins = container_of(led_cdev, struct light, led);
	struct legion_private *priv =
		container_of(light_ins, struct legion_private, ylogo_light);

	return legion_ec_ylogo_set(priv, brightness);
}

static int legion_ec_ylogo_init(struct legion_private *priv)
{
	struct light *light_ins = &priv->ylogo_light;
	int brightness, err;

	if (WARN_ON(light_ins->initialized)) {
		pr_info("Light already initialized for light: Y-Logo\n");
		return -EEXIST;
	}

	brightness = legion_ec_ylogo_get(priv);
	if (brightness < 0) {
		pr_info("Error reading brightness for Y-Logo light\n");
		return brightness;
	}

	light_ins->led.name = "platform::ylogo";
	light_ins->led.max_brightness = 2;
	light_ins->led.brightness_get = legion_ec_ylogo_cdev_brightness_get;
	light_ins->led.brightness_set_blocking =
		legion_ec_ylogo_cdev_brightness_set;
	light_ins->led.flags = LED_BRIGHT_HW_CHANGED;

	err = led_classdev_register(&priv->platform_device->dev,
				    &light_ins->led);
	if (err)
		return err;

	light_ins->initialized = true;
	return 0;
}

static int legion_light_init(struct legion_private *priv,
			     struct light *light_ins, u8 light_id,
			     u8 lower_limit, u8 upper_limit, const char *name)
{
	int brightness, err;

	if (WARN_ON(light_ins->initialized)) {
		pr_info("Light already initialized for light: %u\n",
			light_ins->light_id);
		return -EEXIST;
	}

	light_ins->light_id = light_id;
	light_ins->lower_limit = lower_limit;
	light_ins->upper_limit = upper_limit;

	brightness = legion_wmi_light_get(priv, light_ins->light_id,
					  light_ins->lower_limit,
					  light_ins->upper_limit);
	if (brightness == -ENODEV)
		return brightness;
	if (brightness < 0) {
		pr_info("Error reading brightness for light: %u\n",
			light_ins->light_id);
		return brightness;
	}

	light_ins->led.name = name;
	light_ins->led.max_brightness =
		light_ins->upper_limit - light_ins->lower_limit;
	light_ins->led.brightness_get = legion_wmi_cdev_brightness_get;
	light_ins->led.brightness_set_blocking = legion_wmi_cdev_brightness_set;
	light_ins->led.flags = LED_BRIGHT_HW_CHANGED;

	err = led_classdev_register(&priv->platform_device->dev,
				    &light_ins->led);
	if (err)
		return err;

	light_ins->initialized = true;

	return 0;
}

/**
 * Deinit light.
 *
 * Can also be called if init was not successful.
 *
 */
static void legion_light_exit(struct legion_private *priv,
			      struct light *light_ins)
{
	if (!light_ins->initialized)
		return;

	light_ins->initialized = false;

	led_classdev_unregister(&light_ins->led);
}

/* =============================  */
/* Platform driver                */
/* ============================   */

static int legion_add(struct platform_device *pdev)
{
	struct legion_private *priv;
	const struct dmi_system_id *dmi_sys;
	int err;
	u16 ec_read_id;
	bool skip_ec_id_check;
	bool is_ec_id_valid;
	bool is_denied = true;
	bool is_allowed = false;
	bool do_load_by_list = false;
	bool do_load = false;
	//struct legion_private *priv = dev_get_drvdata(&pdev->dev);
	dev_info(&pdev->dev, "legion_laptop platform driver probing\n");

	dev_info(
		&pdev->dev,
		"Read identifying information: DMI_SYS_VENDOR: %s; DMI_PRODUCT_NAME: %s; DMI_BIOS_VERSION:%s\n",
		dmi_get_system_info(DMI_SYS_VENDOR),
		dmi_get_system_info(DMI_PRODUCT_NAME),
		dmi_get_system_info(DMI_BIOS_VERSION));

	// TODO: allocate?
	priv = &_priv;
	priv->platform_device = pdev;
	err = legion_shared_init(priv);
	if (err) {
		dev_info(&pdev->dev, "legion_laptop is forced to load.\n");
		goto err_legion_shared_init;
	}
	INIT_DELAYED_WORK(&priv->resume_fancurve_work,
			  legion_resume_fancurve_fn);
	dev_set_drvdata(&pdev->dev, priv);

	// TODO: remove
	pr_info("Read identifying information: DMI_SYS_VENDOR: %s; DMI_PRODUCT_NAME: %s; DMI_BIOS_VERSION:%s\n",
		dmi_get_system_info(DMI_SYS_VENDOR),
		dmi_get_system_info(DMI_PRODUCT_NAME),
		dmi_get_system_info(DMI_BIOS_VERSION));

	dmi_sys = dmi_first_match(optimistic_allowlist);
	is_allowed = dmi_sys != NULL;
	is_denied = dmi_check_system(denylist);
	do_load_by_list = is_allowed && !is_denied;
	do_load = do_load_by_list || force;

	dev_info(
		&pdev->dev,
		"is_denied: %d; is_allowed: %d; do_load_by_list: %d; do_load: %d\n",
		is_denied, is_allowed, do_load_by_list, do_load);

	if (!(do_load)) {
		dev_info(
			&pdev->dev,
			"Module not usable for this laptop because it is not in allowlist. Notify the maintainer if you want to add your device or force load with param force.\n");
		err = -ENODEV;
		goto err_model_mismtach;
	}

	if (force)
		dev_info(&pdev->dev, "legion_laptop is forced to load.\n");

	if (!do_load_by_list && do_load) {
		dev_info(
			&pdev->dev,
			"legion_laptop is forced to load and would otherwise not be loaded\n");
	}

	// if forced and no module found, use config for first model
	if (dmi_sys == NULL)
		dmi_sys = &optimistic_allowlist[0];
	dev_info(&pdev->dev, "Using configuration for system: %s\n",
		 dmi_sys->ident);

	priv->conf = dmi_sys->driver_data;
	_model = priv->conf;
	priv->cpu_pl_coupling = priv->conf->has_pl_coupling;

	if (wmi_has_guid(LEGION_WMI_CAPDATA01_GUID)) {
		int instances = wmi_instance_count(LEGION_WMI_CAPDATA01_GUID);

		if (instances > 0) {
			int idx;
			int loaded = 0;

			for (idx = 0;
			     idx < instances && loaded < MAX_CAPDATA_ENTRIES;
			     idx++) {
				union acpi_object *obj;
				struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER,
							   NULL };

				if (ACPI_FAILURE(wmi_query_block(
					    LEGION_WMI_CAPDATA01_GUID, idx,
					    &out)))
					continue;
				obj = out.pointer;
				if (!obj || obj->type != ACPI_TYPE_BUFFER ||
				    obj->buffer.length <
					    sizeof(struct capdata01)) {
					ACPI_FREE(obj);
					continue;
				}
				memcpy(&priv->capdata[loaded],
				       obj->buffer.pointer,
				       sizeof(struct capdata01));
				ACPI_FREE(obj);
				loaded++;
			}
			priv->capdata_count = loaded;
		}
	}

	if (wmi_has_guid(LEGION_WMI_DISCRETE_DATA_GUID)) {
		int instances =
			wmi_instance_count(LEGION_WMI_DISCRETE_DATA_GUID);

		if (instances > 0) {
			int idx;
			int i, fi = 0;
			struct discrete_data_entry entries[MAX_DISCRETE_ENTRIES];
			int entry_count = 0;

			for (idx = 0; idx < instances &&
				      entry_count < MAX_DISCRETE_ENTRIES;
			     idx++) {
				union acpi_object *obj;
				struct acpi_buffer out = { ACPI_ALLOCATE_BUFFER,
							   NULL };

				if (ACPI_FAILURE(wmi_query_block(
					    LEGION_WMI_DISCRETE_DATA_GUID, idx,
					    &out)))
					continue;
				obj = out.pointer;
				if (!obj)
					continue;

				if (obj->type == ACPI_TYPE_PACKAGE &&
				    obj->package.count >= 2 &&
				    obj->package.elements[0].type ==
					    ACPI_TYPE_INTEGER &&
				    obj->package.elements[1].type ==
					    ACPI_TYPE_INTEGER) {
					entries[entry_count].id =
						(u32)obj->package.elements[0]
							.integer.value;
					entries[entry_count].value =
						(u32)obj->package.elements[1]
							.integer.value;
					entry_count++;
				}

				ACPI_FREE(obj);
			}

			for (i = 0;
			     i < entry_count && fi < MAX_DISCRETE_FEATURES;
			     i++) {
				struct discrete_feature *dfe;
				u32 fid = entries[i].id;
				int vi;

				for (vi = 0; vi < fi; vi++) {
					if (priv->discrete_features[vi]
						    .feature_id == fid)
						break;
				}
				dfe = &priv->discrete_features[vi];
				if (vi == fi) {
					dfe->feature_id = fid;
					dfe->count = 0;
					fi++;
				}
				if (dfe->count < ARRAY_SIZE(dfe->values))
					dfe->values[dfe->count++] =
						entries[i].value;
			}
			priv->discrete_feature_count = fi;
		}
	}

	read_powermode(priv, &priv->current_powermode);

#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0)
	err = acpi_init(priv, ACPI_COMPANION(&pdev->dev));
	if (err) {
		dev_info(&pdev->dev, "Could not init ACPI access: %d\n", err);
		goto err_acpi_init;
	}
#else
	err = acpi_init(priv, NULL);
	/*
	 * Do not fail the probe when ACPI init fails on 7.x: the driver
	 * binds a virtual platform device there and the per-model ACPI
	 * paths do not match every DSDT (e.g. LPC0 vs LPCB, or a missing
	 * VPC0._STA), so a failing _STA evaluation is a false negative.
	 * ACPI is only one access method; EC RAM and WMI stay fully
	 * functional without it. Check and log the error, but carry on.
	 */
	if (err)
		dev_info(&pdev->dev,
			 "Could not init ACPI access: %d; continuing without ACPI\n",
			 err);
#endif
	// TODO: remove; only used for reverse engineering
	pr_info("Creating RAM access to embedded controller\n");
	err = ecram_memoryio_init(&priv->ec_memoryio,
				  priv->conf->ramio_physical_start, 0,
				  priv->conf->ramio_size);
	if (err) {
		dev_info(
			&pdev->dev,
			"Could not init RAM access to embedded controller: %d\n",
			err);
		goto err_ecram_memoryio_init;
	}

	err = ecram_init(&priv->ecram, priv->conf->memoryio_physical_ec_start,
			 priv->conf->memoryio_size);
	if (err) {
		dev_info(&pdev->dev,
			 "Could not init access to embedded controller: %d\n",
			 err);
		goto err_ecram_init;
	}

	ec_read_id = read_ec_id(&priv->ecram, priv->conf);
	dev_info(&pdev->dev, "Read embedded controller ID 0x%x\n", ec_read_id);
	skip_ec_id_check = force || (!priv->conf->check_embedded_controller_id);
	is_ec_id_valid = skip_ec_id_check ||
			 (ec_read_id == priv->conf->embedded_controller_id);
	if (!is_ec_id_valid) {
		err = -EIO;
		dev_info(&pdev->dev, "Expected EC chip id 0x%x but read 0x%x\n",
			 priv->conf->embedded_controller_id, ec_read_id);
		goto err_ecram_id;
	}
	if (skip_ec_id_check) {
		dev_info(&pdev->dev,
			 "Skipped checking embedded controller id\n");
	}

	dev_info(&pdev->dev, "Creating debugfs interface\n");
	legion_debugfs_init(priv);

	pr_info("Creating sysfs interface\n");
	err = legion_sysfs_init(priv);
	if (err) {
		dev_info(&pdev->dev, "Failed to create sysfs interface: %d\n",
			 err);
		goto err_sysfs_init;
	}

	pr_info("Creating hwmon interface");
	err = legion_hwmon_init(priv);
	if (err) {
		dev_info(&pdev->dev, "Failed to create hwmon interface: %d\n",
			 err);
		goto err_hwmon_init;
	}

	pr_info("Creating platform profile support\n");
	err = legion_platform_profile_init(priv);
	if (err) {
		dev_info(&pdev->dev, "Failed to create platform profile: %d\n",
			 err);
		goto err_platform_profile;
	}

	pr_info("Init WMI driver support\n");
	err = legion_wmi_init();
	if (err) {
		dev_info(&pdev->dev, "Failed to init WMI driver: %d\n", err);
		goto err_wmi;
	}

	pr_info("Init keyboard backlight LED driver\n");
	err = legion_kbd_bl_init(priv);
	if (err) {
		dev_info(
			&pdev->dev,
			"Failed to init keyboard backlight LED driver. Skipping ...\n");
	}

	if (priv->conf->ec_ylogo_register) {
		pr_info("Init Y-Logo LED driver\n");
		err = legion_ec_ylogo_init(priv);
		if (err) {
			dev_info(
				&pdev->dev,
				"Failed to init Y-Logo LED driver. Skipping ...\n");
		}
	} else if (!priv->conf->skip_ylogo_light) {
		pr_info("Init Y-Logo LED driver\n");
		err = legion_light_init(priv, &priv->ylogo_light,
					LIGHT_ID_YLOGO, 0, 1,
					"platform::ylogo");
		if (err) {
			dev_info(
				&pdev->dev,
				"Failed to init Y-Logo LED driver. Skipping ...\n");
		}
	}

	if (!priv->conf->skip_ioport_light) {
		pr_info("Init IO-Port LED driver\n");
		err = legion_light_init(priv, &priv->iport_light,
					LIGHT_ID_IOPORT, 0, 2,
					"platform::ioport");
		if (err && err != -ENODEV) {
			dev_info(
				&pdev->dev,
				"Failed to init IO-Port LED driver. Skipping ...\n");
		}
	}

	dev_info(&pdev->dev, "legion_laptop loaded for this device\n");
	return 0;

	// TODO: remove eventually
	legion_light_exit(priv, &priv->iport_light);
	legion_light_exit(priv, &priv->ylogo_light);
	legion_kbd_bl_exit(priv);
	legion_wmi_exit();
err_wmi:
	legion_platform_profile_exit(priv);
err_platform_profile:
	legion_hwmon_exit(priv);
err_hwmon_init:
	legion_sysfs_exit(priv);
err_sysfs_init:
	legion_debugfs_exit(priv);
err_ecram_id:
	ecram_exit(&priv->ecram);
err_ecram_init:
	ecram_memoryio_exit(&priv->ec_memoryio);
err_ecram_memoryio_init:
#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0)
err_acpi_init:
#endif
	acpi_exit(priv);
	legion_shared_exit(priv);
err_legion_shared_init:
err_model_mismtach:
	dev_info(&pdev->dev, "legion_laptop not loaded for this device\n");
	return err;
}

static void legion_remove(struct platform_device *pdev)
{
	struct legion_private *priv = dev_get_drvdata(&pdev->dev);

	mutex_lock(&legion_shared_mutex);
	priv->loaded = false;
	mutex_unlock(&legion_shared_mutex);

	cancel_delayed_work_sync(&priv->resume_fancurve_work);
	legion_light_exit(priv, &priv->iport_light);
	legion_light_exit(priv, &priv->ylogo_light);
	legion_kbd_bl_exit(priv);
	// first unregister wmi, so toggling powermode does not
	// generate events anymore that even might be delayed
	legion_wmi_exit();
	legion_platform_profile_exit(priv);

	// toggle power mode to load default setting from embedded controller
	// again
	toggle_powermode(priv);

	legion_hwmon_exit(priv);
	legion_sysfs_exit(priv);
	legion_debugfs_exit(priv);
	ecram_exit(&priv->ecram);
	ecram_memoryio_exit(&priv->ec_memoryio);
	acpi_exit(priv);
	legion_shared_exit(priv);

	pr_info("Legion platform unloaded\n");
}

static int legion_resume(struct platform_device *pdev)
{
	dev_info(&pdev->dev, "Resumed in legion-laptop\n");
	legion_resume_fancurve(dev_get_drvdata(&pdev->dev));

	return 0;
}

#ifdef CONFIG_PM_SLEEP
static int legion_pm_resume(struct device *dev)
{
	dev_info(dev, "Resumed PM in legion-laptop\n");
	legion_resume_fancurve(dev_get_drvdata(dev));

	return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(legion_pm, NULL, legion_pm_resume);

// same as ideapad
static const struct acpi_device_id legion_device_ids[] = {
// todo: change to "VPC2004", and also ACPI paths
#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0)
	{ "PNP0C09", 0 },
#endif
	{ "", 0 },
};
MODULE_DEVICE_TABLE(acpi, legion_device_ids);

static struct platform_driver legion_driver = {
	.probe = legion_add,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0)
	.remove = legion_remove,
#else
	.remove_new = legion_remove,
#endif
	.resume = legion_resume,
	.driver = {
		.name   = "legion",
#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0) //leave as virtual driver
		.pm     = &legion_pm,
		.acpi_match_table = ACPI_PTR(legion_device_ids),
#endif
	},
};

static int __init legion_init(void)
{
	int err;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
	static struct platform_device *legion_pdev;
#endif
	pr_info("Loading legion_laptop\n");
	err = platform_driver_register(&legion_driver);
	if (err) {
		pr_info("legion_laptop: platform_driver_register failed\n");
		return err;
	}
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
	legion_pdev = platform_device_register_simple("legion", -1, NULL, 0);
	if (IS_ERR(legion_pdev)) {
		pr_err("Failed to allocate virtual legion device\n");
		platform_driver_unregister(&legion_driver);
		return PTR_ERR(legion_pdev);
	}
#endif
	return 0;
}

module_init(legion_init);

static void __exit legion_exit(void)
{
	platform_driver_unregister(&legion_driver);
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
	platform_device_unregister(_priv.platform_device);
#endif
	pr_info("legion_laptop exit\n");
}

module_exit(legion_exit);
