diff --git a/lact-client/src/lib.rs b/lact-client/src/lib.rs index 7e6cd84..95d9e45 100644 --- a/lact-client/src/lib.rs +++ b/lact-client/src/lib.rs @@ -10,8 +10,8 @@ use anyhow::{anyhow, Context}; use nix::unistd::getuid; use schema::{ request::{ConfirmCommand, SetClocksCommand}, - ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions, - PowerStates, Request, Response, SystemInfo, + ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request, + Response, SystemInfo, }; use serde::Deserialize; use std::{ @@ -104,24 +104,8 @@ impl DaemonClient { self.make_request(Request::ListDevices) } - pub fn set_fan_control( - &self, - id: &str, - enabled: bool, - mode: Option, - static_speed: Option, - curve: Option, - pmfw: PmfwOptions, - ) -> anyhow::Result { - self.make_request(Request::SetFanControl { - id, - enabled, - mode, - static_speed, - curve, - pmfw, - })? - .inner() + pub fn set_fan_control(&self, cmd: FanOptions) -> anyhow::Result { + self.make_request(Request::SetFanControl(cmd))?.inner() } pub fn set_power_cap(&self, id: &str, cap: Option) -> anyhow::Result { diff --git a/lact-daemon/src/config.rs b/lact-daemon/src/config.rs index a4630db..9cad5fc 100644 --- a/lact-daemon/src/config.rs +++ b/lact-daemon/src/config.rs @@ -109,6 +109,8 @@ pub struct FanControlSettings { pub temperature_key: String, pub interval_ms: u64, pub curve: FanCurve, + pub spindown_delay_ms: Option, + pub change_threshold: Option, } impl Default for FanControlSettings { @@ -119,6 +121,8 @@ impl Default for FanControlSettings { temperature_key: "edge".to_owned(), interval_ms: 500, curve: FanCurve(default_fan_curve()), + spindown_delay_ms: None, + change_threshold: None, } } } @@ -228,11 +232,10 @@ fn default_apply_settings_timer() -> u64 { #[cfg(test)] mod tests { - use lact_schema::{FanControlMode, PmfwOptions}; - use std::collections::HashMap; - use super::{ClocksConfiguration, Config, Daemon, FanControlSettings, Gpu}; use crate::server::gpu_controller::fan_control::FanCurve; + use lact_schema::{FanControlMode, PmfwOptions}; + use std::collections::HashMap; #[test] fn serde_de_full() { @@ -248,6 +251,8 @@ mod tests { interval_ms: 500, mode: FanControlMode::Curve, static_speed: 0.5, + spindown_delay_ms: Some(5000), + change_threshold: Some(3), }), ..Default::default() }, diff --git a/lact-daemon/src/server/gpu_controller/mod.rs b/lact-daemon/src/server/gpu_controller/mod.rs index e786f2b..fc9cacc 100644 --- a/lact-daemon/src/server/gpu_controller/mod.rs +++ b/lact-daemon/src/server/gpu_controller/mod.rs @@ -2,7 +2,7 @@ pub mod fan_control; use self::fan_control::FanCurve; use super::vulkan::get_vulkan_info; -use crate::config::{self, ClocksConfiguration}; +use crate::config::{self, ClocksConfiguration, FanControlSettings}; use amdgpu_sysfs::{ error::Error, gpu_handle::{ @@ -19,7 +19,6 @@ use lact_schema::{ PciInfo, PmfwInfo, PowerState, PowerStates, PowerStats, VoltageStats, VramStats, }; use pciid_parser::Database; -use std::collections::BTreeMap; use std::{ borrow::Cow, cell::RefCell, @@ -29,6 +28,7 @@ use std::{ str::FromStr, time::Duration, }; +use std::{collections::BTreeMap, time::Instant}; use tokio::{ select, sync::Notify, @@ -250,20 +250,17 @@ impl GpuController { } pub fn get_stats(&self, gpu_config: Option<&config::Gpu>) -> DeviceStats { + let fan_settings = gpu_config.and_then(|config| config.fan_control_settings.as_ref()); DeviceStats { fan: FanStats { control_enabled: gpu_config .map(|config| config.fan_control_enabled) .unwrap_or_default(), - control_mode: gpu_config - .and_then(|config| config.fan_control_settings.as_ref()) - .map(|settings| settings.mode), - static_speed: gpu_config - .and_then(|config| config.fan_control_settings.as_ref()) - .map(|settings| settings.static_speed), - curve: gpu_config - .and_then(|config| config.fan_control_settings.as_ref()) - .map(|settings| settings.curve.0.clone()), + control_mode: fan_settings.map(|settings| settings.mode), + static_speed: fan_settings.map(|settings| settings.static_speed), + curve: fan_settings.map(|settings| settings.curve.0.clone()), + spindown_delay_ms: fan_settings.and_then(|settings| settings.spindown_delay_ms), + change_threshold: fan_settings.and_then(|settings| settings.change_threshold), speed_current: self.hw_mon_and_then(HwMon::get_fan_current), speed_max: self.hw_mon_and_then(HwMon::get_fan_max), speed_min: self.hw_mon_and_then(HwMon::get_fan_min), @@ -423,8 +420,7 @@ impl GpuController { async fn start_curve_fan_control( &self, curve: FanCurve, - temp_key: String, - interval: Duration, + settings: FanControlSettings, ) -> anyhow::Result<()> { // Use the PMFW curve functionality when it is available // Otherwise, fall back to manual fan control via a task @@ -441,18 +437,14 @@ impl GpuController { Ok(()) } - Err(_) => { - self.start_curve_fan_control_task(curve, temp_key, interval) - .await - } + Err(_) => self.start_curve_fan_control_task(curve, settings).await, } } async fn start_curve_fan_control_task( &self, curve: FanCurve, - temp_key: String, - interval: Duration, + settings: FanControlSettings, ) -> anyhow::Result<()> { // Stop existing task to re-apply new curve self.stop_fan_control(false).await?; @@ -475,7 +467,17 @@ impl GpuController { let notify = Rc::new(Notify::new()); let task_notify = notify.clone(); + debug!("spawning new fan control task"); let handle = tokio::task::spawn_local(async move { + let mut last_pwm = (None, Instant::now()); + let mut last_temp = 0.0; + + let temp_key = settings.temperature_key.clone(); + let interval = Duration::from_millis(settings.interval_ms); + let spindown_delay = Duration::from_millis(settings.spindown_delay_ms.unwrap_or(0)); + #[allow(clippy::cast_precision_loss)] + let change_threshold = settings.change_threshold.unwrap_or(0) as f32; + loop { select! { () = sleep(interval) => (), @@ -486,7 +488,31 @@ impl GpuController { let temp = temps .remove(&temp_key) .expect("Could not get temperature by given key"); + + let current_temp = temp.current.expect("Missing temp"); + + if (last_temp - current_temp).abs() < change_threshold { + trace!("temperature changed from {last_temp}°C to {current_temp}°C, which is less than the {change_threshold}°C threshold, skipping speed adjustment"); + continue; + } + let target_pwm = curve.pwm_at_temp(temp); + let now = Instant::now(); + + if let (Some(previous_pwm), previous_timestamp) = last_pwm { + let diff = now - previous_timestamp; + if target_pwm < previous_pwm && diff < spindown_delay { + trace!( + "delaying fan spindown ({}ms left)", + (spindown_delay - diff).as_millis() + ); + continue; + } + } + + last_pwm = (Some(target_pwm), now); + last_temp = current_temp; + trace!("fan control tick: setting pwm to {target_pwm}"); if let Err(err) = hw_mon.set_fan_pwm(target_pwm) { @@ -501,7 +527,7 @@ impl GpuController { debug!( "started fan control with interval {}ms", - interval.as_millis() + settings.interval_ms ); Ok(()) @@ -742,14 +768,9 @@ impl GpuController { return Err(anyhow!("Cannot use empty fan curve")); } - let interval = Duration::from_millis(settings.interval_ms); - self.start_curve_fan_control( - settings.curve.clone(), - settings.temperature_key.clone(), - interval, - ) - .await - .context("Failed to set curve fan control")?; + self.start_curve_fan_control(settings.curve.clone(), settings.clone()) + .await + .context("Failed to set curve fan control")?; } } } else { diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index 3e9ccd7..7aabe17 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -11,7 +11,7 @@ use anyhow::{anyhow, Context}; use lact_schema::{ default_fan_curve, request::{ConfirmCommand, SetClocksCommand}, - ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions, + ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions, PowerStates, }; use libflate::gzip; @@ -270,40 +270,35 @@ impl<'a> Handler { self.controller_by_id(id)?.get_clocks_info() } - pub async fn set_fan_control( - &'a self, - id: &str, - enabled: bool, - mode: Option, - static_speed: Option, - curve: Option, - pmfw: PmfwOptions, - ) -> anyhow::Result { + pub async fn set_fan_control(&'a self, opts: FanOptions<'_>) -> anyhow::Result { let settings = { let mut config_guard = self .config .try_borrow_mut() .map_err(|err| anyhow!("{err}"))?; - let gpu_config = config_guard.gpus.entry(id.to_owned()).or_default(); + let gpu_config = config_guard.gpus.entry(opts.id.to_owned()).or_default(); - match mode { + match opts.mode { Some(mode) => match mode { FanControlMode::Static => { - if matches!(static_speed, Some(speed) if !(0.0..=1.0).contains(&speed)) { + if matches!(opts.static_speed, Some(speed) if !(0.0..=1.0).contains(&speed)) + { return Err(anyhow!("static speed value out of range")); } if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone() { existing_settings.mode = mode; - if let Some(static_speed) = static_speed { + if let Some(static_speed) = opts.static_speed { existing_settings.static_speed = static_speed; } Some(existing_settings) } else { Some(FanControlSettings { mode, - static_speed: static_speed.unwrap_or_else(default_fan_static_speed), + static_speed: opts + .static_speed + .unwrap_or_else(default_fan_static_speed), ..Default::default() }) } @@ -312,18 +307,27 @@ impl<'a> Handler { if let Some(mut existing_settings) = gpu_config.fan_control_settings.clone() { existing_settings.mode = mode; - if let Some(raw_curve) = curve { + if let Some(change_threshold) = opts.change_threshold { + existing_settings.change_threshold = Some(change_threshold); + } + if let Some(spindown_delay) = opts.spindown_delay_ms { + existing_settings.spindown_delay_ms = Some(spindown_delay); + } + + if let Some(raw_curve) = opts.curve { let curve = FanCurve(raw_curve); curve.validate()?; existing_settings.curve = curve; } Some(existing_settings) } else { - let curve = FanCurve(curve.unwrap_or_else(default_fan_curve)); + let curve = FanCurve(opts.curve.unwrap_or_else(default_fan_curve)); curve.validate()?; Some(FanControlSettings { mode, curve, + change_threshold: opts.change_threshold, + spindown_delay_ms: opts.spindown_delay_ms, ..Default::default() }) } @@ -333,12 +337,12 @@ impl<'a> Handler { } }; - self.edit_gpu_config(id.to_owned(), |config| { - config.fan_control_enabled = enabled; + self.edit_gpu_config(opts.id.to_owned(), |config| { + config.fan_control_enabled = opts.enabled; if let Some(settings) = settings { config.fan_control_settings = Some(settings); } - config.pmfw_options = pmfw; + config.pmfw_options = opts.pmfw; }) .await .context("Failed to edit GPU config") diff --git a/lact-daemon/src/server/mod.rs b/lact-daemon/src/server/mod.rs index 78e806f..1eeb7c0 100644 --- a/lact-daemon/src/server/mod.rs +++ b/lact-daemon/src/server/mod.rs @@ -12,7 +12,7 @@ use tokio::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::{UnixListener, UnixStream}, }; -use tracing::{debug, error, instrument}; +use tracing::{error, instrument, trace}; pub struct Server { pub handler: Handler, @@ -52,7 +52,7 @@ pub async fn handle_stream(stream: UnixStream, handler: Handler) -> anyhow::Resu let mut buf = String::new(); while stream.read_line(&mut buf).await? != 0 { - debug!("handling request: {}", buf.trim_end()); + trace!("handling request: {}", buf.trim_end()); let maybe_request = serde_json::from_str(&buf); let response = match maybe_request { @@ -86,18 +86,7 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho Request::DevicePowerProfileModes { id } => { ok_response(handler.get_power_profile_modes(id)?) } - Request::SetFanControl { - id, - enabled, - mode, - static_speed, - curve, - pmfw, - } => ok_response( - handler - .set_fan_control(id, enabled, mode, static_speed, curve, pmfw) - .await?, - ), + Request::SetFanControl(opts) => ok_response(handler.set_fan_control(opts).await?), Request::ResetPmfw { id } => ok_response(handler.reset_pmfw(id).await?), Request::SetPowerCap { id, cap } => ok_response(handler.set_power_cap(id, cap).await?), Request::SetPerformanceLevel { @@ -127,7 +116,7 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho } fn ok_response(data: T) -> anyhow::Result> { - debug!("responding with {data:?}"); + trace!("responding with {data:?}"); Ok(serde_json::to_vec(&Response::Ok(data))?) } diff --git a/lact-gui/src/app/mod.rs b/lact-gui/src/app/mod.rs index 37d2e2f..b99018e 100644 --- a/lact-gui/src/app/mod.rs +++ b/lact-gui/src/app/mod.rs @@ -13,7 +13,7 @@ use gtk::glib::{timeout_future, ControlFlow}; use gtk::{gio::ApplicationFlags, prelude::*, *}; use header::Header; use lact_client::schema::request::{ConfirmCommand, SetClocksCommand}; -use lact_client::schema::GIT_COMMIT; +use lact_client::schema::{FanOptions, GIT_COMMIT}; use lact_client::DaemonClient; use lact_daemon::MODULE_CONF_PATH; use root_stack::RootStack; @@ -408,16 +408,19 @@ impl App { if let Some(thermals_settings) = self.root_stack.thermals_page.get_thermals_settings() { debug!("applying thermal settings: {thermals_settings:?}"); + let opts = FanOptions { + id: &gpu_id, + enabled: thermals_settings.manual_fan_control, + mode: thermals_settings.mode, + static_speed: thermals_settings.static_speed, + curve: thermals_settings.curve, + pmfw: thermals_settings.pmfw, + spindown_delay_ms: thermals_settings.spindown_delay_ms, + change_threshold: thermals_settings.change_threshold, + }; self.daemon_client - .set_fan_control( - &gpu_id, - thermals_settings.manual_fan_control, - thermals_settings.mode, - thermals_settings.static_speed, - thermals_settings.curve, - thermals_settings.pmfw, - ) + .set_fan_control(opts) .context("Could not set fan control")?; self.daemon_client .confirm_pending_config(ConfirmCommand::Confirm) diff --git a/lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs b/lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs index bacc703..041f1d2 100644 --- a/lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs +++ b/lact-gui/src/app/root_stack/thermals_page/fan_curve_frame/mod.rs @@ -1,6 +1,7 @@ mod point_adjustment; use self::point_adjustment::PointAdjustment; +use crate::app::root_stack::oc_adjustment::OcAdjustment; use glib::clone; use gtk::graphene::Point; use gtk::gsk::Transform; @@ -11,11 +12,17 @@ use std::cell::RefCell; use std::collections::BTreeMap; use std::rc::Rc; +const DEFAULT_CHANGE_THRESHOLD: u64 = 2; +const DEFAULT_SPINDOWN_DELAY_MS: u64 = 5000; + #[derive(Clone)] pub struct FanCurveFrame { pub container: Box, curve_container: Frame, points: Rc>>, + spindown_delay_adj: OcAdjustment, + change_threshold_adj: OcAdjustment, + hysteresis_grid: Grid, } impl FanCurveFrame { @@ -68,15 +75,55 @@ impl FanCurveFrame { let points = Rc::new(RefCell::new(Vec::new())); + let hysteresis_grid = Grid::new(); + hysteresis_grid.set_margin_top(10); + + let spindown_delay_adj = oc_adjustment_row( + &hysteresis_grid, + 0, + "Spindown delay", + "How long the GPU needs to remain at a lower temperature point for before ramping down the fan", + " ms", + OcAdjustmentOptions { + default: DEFAULT_SPINDOWN_DELAY_MS as f64, + min: 0.0, + max: 30_000.0, + step: 10.0, + digits: 0, + }, + ); + + let change_threshold_adj = oc_adjustment_row( + &hysteresis_grid, + 1, + "Speed change threshold", + "Hysteresis", + "°C", + OcAdjustmentOptions { + default: DEFAULT_CHANGE_THRESHOLD as f64, + min: 0.0, + max: 10.0, + step: 1.0, + digits: 0, + }, + ); + + root_box.append(&hysteresis_grid); + let curve_frame = Self { container: root_box, curve_container, points, + spindown_delay_adj: spindown_delay_adj.clone(), + change_threshold_adj: change_threshold_adj.clone(), + hysteresis_grid, }; default_button.connect_clicked(clone!(@strong curve_frame => move |_| { let curve = default_fan_curve(); curve_frame.set_curve(&curve); + spindown_delay_adj.set_value(DEFAULT_SPINDOWN_DELAY_MS as f64); + change_threshold_adj.set_value(DEFAULT_CHANGE_THRESHOLD as f64); })); add_button.connect_clicked(clone!(@strong curve_frame => move |_| { @@ -148,6 +195,15 @@ impl FanCurveFrame { } pub fn connect_adjusted(&self, f: F) { + self.change_threshold_adj + .connect_value_changed(clone!(@strong f => move |_| { + f(); + })); + self.spindown_delay_adj + .connect_value_changed(clone!(@strong f => move |_| { + f(); + })); + let closure = clone!(@strong f => move |_: &Adjustment| { f(); }); @@ -157,6 +213,97 @@ impl FanCurveFrame { point.temperature.connect_value_changed(closure.clone()); } } + + pub fn set_change_threshold(&self, value: Option) { + self.change_threshold_adj + .set_initial_value(value.unwrap_or(0) as f64); + } + + pub fn set_spindown_delay_ms(&self, value: Option) { + self.spindown_delay_adj + .set_initial_value(value.unwrap_or(0) as f64); + } + + pub fn get_change_threshold(&self) -> u64 { + self.change_threshold_adj.value() as u64 + } + + pub fn get_spindown_delay_ms(&self) -> u64 { + self.spindown_delay_adj.value() as u64 + } + + pub fn set_hysteresis_settings_visibile(&self, visible: bool) { + self.hysteresis_grid.set_visible(visible); + } +} + +struct OcAdjustmentOptions { + default: f64, + min: f64, + max: f64, + step: f64, + digits: i32, +} + +fn oc_adjustment_row( + grid: &Grid, + row: i32, + label: &str, + tooltip: &str, + unit: &'static str, + opts: OcAdjustmentOptions, +) -> OcAdjustment { + let label = Label::builder() + .label(label) + .halign(Align::Start) + .tooltip_text(tooltip) + .build(); + let adjustment = OcAdjustment::new( + opts.default, + opts.min, + opts.max, + opts.step, + opts.step, + opts.step, + ); + + let scale = Scale::builder() + .orientation(Orientation::Horizontal) + .adjustment(&adjustment) + .hexpand(true) + .round_digits(opts.digits) + .digits(opts.digits) + .value_pos(PositionType::Right) + .margin_start(5) + .margin_end(5) + .build(); + + let value_selector = SpinButton::new(Some(&adjustment), opts.step, opts.digits as u32); + + let value_label = Label::new(Some(&format!("{}{unit}", opts.default))); + + let popover = Popover::builder().child(&value_selector).build(); + let value_button = MenuButton::builder() + .popover(&popover) + .child(&value_label) + .build(); + + adjustment.connect_value_changed(clone!(@strong value_label => move |adjustment| { + let value = match opts.digits { + 0 => adjustment.value().round(), + _ => { + let rounding = opts.digits as f64 * 10.0; + (adjustment.value() * rounding).round() / rounding + } + }; + value_label.set_text(&format!("{value}{unit}")); + })); + + grid.attach(&label, 0, row, 1, 1); + grid.attach(&scale, 1, row, 4, 1); + grid.attach(&value_button, 6, row, 4, 1); + + adjustment } #[cfg(all(test, feature = "gtk-tests"))] diff --git a/lact-gui/src/app/root_stack/thermals_page/mod.rs b/lact-gui/src/app/root_stack/thermals_page/mod.rs index 32db0a1..a500a07 100644 --- a/lact-gui/src/app/root_stack/thermals_page/mod.rs +++ b/lact-gui/src/app/root_stack/thermals_page/mod.rs @@ -5,7 +5,7 @@ use glib::clone; use gtk::prelude::*; use gtk::*; use lact_client::schema::{ - default_fan_curve, DeviceInfo, DeviceStats, FanControlMode, FanCurveMap, PmfwOptions, + default_fan_curve, DeviceInfo, DeviceStats, FanControlMode, FanCurveMap, PmfwInfo, PmfwOptions, SystemInfo, }; use lact_daemon::AMDGPU_FAMILY_GC_11_0_0; @@ -25,6 +25,8 @@ pub struct ThermalsSettings { pub static_speed: Option, pub curve: Option, pub pmfw: PmfwOptions, + pub spindown_delay_ms: Option, + pub change_threshold: Option, } #[derive(Clone)] @@ -195,6 +197,15 @@ impl ThermalsPage { self.fan_curve_frame.set_curve(curve); } + self.fan_curve_frame + .set_spindown_delay_ms(stats.fan.spindown_delay_ms); + self.fan_curve_frame + .set_change_threshold(stats.fan.change_threshold); + + // Only show hysteresis settings when PMFW is not used + self.fan_curve_frame + .set_hysteresis_settings_visibile(stats.fan.pmfw_info == PmfwInfo::default()); + if !stats.fan.control_enabled && self.fan_curve_frame.get_curve().is_empty() { self.fan_curve_frame.set_curve(&default_fan_curve()); } @@ -246,6 +257,8 @@ impl ThermalsPage { static_speed, curve, pmfw, + change_threshold: Some(self.fan_curve_frame.get_change_threshold()), + spindown_delay_ms: Some(self.fan_curve_frame.get_spindown_delay_ms()), }) } else { None diff --git a/lact-schema/src/lib.rs b/lact-schema/src/lib.rs index 51845eb..9acfb09 100644 --- a/lact-schema/src/lib.rs +++ b/lact-schema/src/lib.rs @@ -197,6 +197,8 @@ pub struct FanStats { pub speed_current: Option, pub speed_max: Option, pub speed_min: Option, + pub spindown_delay_ms: Option, + pub change_threshold: Option, // RDNA3+ params #[serde(default)] pub pmfw_info: PmfwInfo, @@ -273,3 +275,17 @@ pub struct PmfwOptions { pub minimum_pwm: Option, pub target_temperature: Option, } + +#[skip_serializing_none] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] +pub struct FanOptions<'a> { + pub id: &'a str, + pub enabled: bool, + pub mode: Option, + pub static_speed: Option, + pub curve: Option, + #[serde(default)] + pub pmfw: PmfwOptions, + pub spindown_delay_ms: Option, + pub change_threshold: Option, +} diff --git a/lact-schema/src/request.rs b/lact-schema/src/request.rs index 977848b..a03149e 100644 --- a/lact-schema/src/request.rs +++ b/lact-schema/src/request.rs @@ -1,4 +1,4 @@ -use crate::{FanControlMode, FanCurveMap, PmfwOptions}; +use crate::FanOptions; use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind}; use serde::{Deserialize, Serialize}; @@ -20,15 +20,7 @@ pub enum Request<'a> { DevicePowerProfileModes { id: &'a str, }, - SetFanControl { - id: &'a str, - enabled: bool, - mode: Option, - static_speed: Option, - curve: Option, - #[serde(default)] - pmfw: PmfwOptions, - }, + SetFanControl(FanOptions<'a>), ResetPmfw { id: &'a str, }, diff --git a/lact-schema/src/tests.rs b/lact-schema/src/tests.rs index a9edb95..8da4986 100644 --- a/lact-schema/src/tests.rs +++ b/lact-schema/src/tests.rs @@ -1,6 +1,7 @@ -use crate::{Pong, Request, Response}; +use crate::{FanControlMode, FanOptions, PmfwOptions, Pong, Request, Response}; use anyhow::anyhow; use serde_json::json; +use std::collections::BTreeMap; #[test] fn ping_requset() { @@ -57,3 +58,31 @@ fn error_response() { assert_eq!(serde_json::to_value(response).unwrap(), expected_response); } + +#[test] +fn set_fan_clocks() { + let value = r#"{ + "command": "set_fan_control", + "args": { + "id": "123", + "enabled": true, + "mode": "curve", + "curve": { + "30": 30.0, + "50": 50.0 + } + } + }"#; + let request: Request = serde_json::from_str(value).unwrap(); + let expected_request = Request::SetFanControl(FanOptions { + id: "123", + enabled: true, + mode: Some(FanControlMode::Curve), + static_speed: None, + curve: Some(BTreeMap::from([(30, 30.0), (50, 50.0)])), + pmfw: PmfwOptions::default(), + spindown_delay_ms: None, + change_threshold: None, + }); + assert_eq!(expected_request, request); +}