From 21e622345515fb198f81b1c82707a9720a81bbad Mon Sep 17 00:00:00 2001 From: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:47:44 +0000 Subject: [PATCH 1/4] feat(power): expose factory default/min/max power target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BOS+ (and other firmwares) report the power-target the device ships with — the value all tuning starts from — plus the accepted min/max. Surface it: - Add DefaultPowerTarget/MinPowerTarget/MaxPowerTarget DataFields and the matching Option fields on MinerData, with GetDefaultPowerTarget / GetMinPowerTarget / GetMaxPowerTarget traits (default to None) wired into the GetMinerData supertrait + blanket impl. - Braiins v26.04: read bosminer.metadata.autotuning.powerTarget.{default,min,max} via the existing GraphQL client and parse them in the new traits. - All other backends get trivial (None) impls. - Python bindings + stubs + interop fixture updated. --- asic-rs-core/src/data/collector.rs | 9 +++ asic-rs-core/src/data/miner.rs | 10 ++++ asic-rs-core/src/traits/miner.rs | 60 +++++++++++++++++++ .../antminer/src/backends/v2020/mod.rs | 3 + .../antminer/src/backends/v2023_07/mod.rs | 3 + .../auradine/src/backends/v1/mod.rs | 4 +- .../avalonminer/src/backends/avalon_a/mod.rs | 4 +- .../avalonminer/src/backends/avalon_q/mod.rs | 4 +- .../bitaxe/src/backends/v2_0_0/mod.rs | 3 + .../bitaxe/src/backends/v2_9_0/mod.rs | 3 + .../braiins/src/backends/v21_09/mod.rs | 3 + .../braiins/src/backends/v25_03/mod.rs | 3 + .../braiins/src/backends/v25_05/mod.rs | 3 + .../braiins/src/backends/v25_07/mod.rs | 3 + .../braiins/src/backends/v26_04/mod.rs | 54 +++++++++++++++++ asic-rs-firmwares/epic/src/backends/v1/mod.rs | 3 + .../futurebit/src/backends/v2/mod.rs | 4 +- .../luxminer/src/backends/v1/mod.rs | 4 +- .../marathon/src/backends/v1/mod.rs | 4 +- .../nerdaxe/src/backends/v1/mod.rs | 3 + .../proto/src/backends/v1/mod.rs | 4 +- .../sealminer/src/backends/v2025/mod.rs | 4 +- .../vnish/src/backends/v1_2_0/mod.rs | 4 +- .../whatsminer/src/backends/v1/mod.rs | 3 + .../whatsminer/src/backends/v2/mod.rs | 3 + .../whatsminer/src/backends/v3/mod.rs | 3 + python/pyasic_rs/asic_rs.pyi | 9 +++ python/tests/test_pydantic_interop.py | 3 + 28 files changed, 211 insertions(+), 9 deletions(-) diff --git a/asic-rs-core/src/data/collector.rs b/asic-rs-core/src/data/collector.rs index 04655310..cc8bf50a 100644 --- a/asic-rs-core/src/data/collector.rs +++ b/asic-rs-core/src/data/collector.rs @@ -77,6 +77,15 @@ pub enum DataField { /// Configured tuning target (power or hashrate). #[cfg_attr(feature = "python", pydantic(value = "TuningTarget"))] TuningTarget, + /// Factory default power target the miner ships with (the value all tuning starts from). + #[cfg_attr(feature = "python", pydantic(value = "DefaultPowerTarget"))] + DefaultPowerTarget, + /// Lowest power target the miner accepts. + #[cfg_attr(feature = "python", pydantic(value = "MinPowerTarget"))] + MinPowerTarget, + /// Highest power target the miner accepts. + #[cfg_attr(feature = "python", pydantic(value = "MaxPowerTarget"))] + MaxPowerTarget, /// Efficiency of the miner (e.g., J/TH). #[cfg_attr(feature = "python", pydantic(value = "Efficiency"))] Efficiency, diff --git a/asic-rs-core/src/data/miner.rs b/asic-rs-core/src/data/miner.rs index d86c6da1..56f22d75 100644 --- a/asic-rs-core/src/data/miner.rs +++ b/asic-rs-core/src/data/miner.rs @@ -118,6 +118,16 @@ pub struct MinerData { pub tuning_target: Option, /// The current tuning target adjusted by scaling settings, when available. pub scaled_tuning_target: Option, + /// The factory default power target the miner ships with, i.e. the value all + /// tuning starts from. Reported by firmwares that expose tuner metadata. + #[serde(serialize_with = "serialize_power")] + pub default_power_target: Option, + /// The lowest power target the miner accepts, when reported. + #[serde(serialize_with = "serialize_power")] + pub min_power_target: Option, + /// The highest power target the miner accepts, when reported. + #[serde(serialize_with = "serialize_power")] + pub max_power_target: Option, /// The current efficiency in W/TH/s (J/TH) of the miner pub efficiency: Option, /// The state of the fault/alert light on the miner diff --git a/asic-rs-core/src/traits/miner.rs b/asic-rs-core/src/traits/miner.rs index 200aa177..510878b8 100644 --- a/asic-rs-core/src/traits/miner.rs +++ b/asic-rs-core/src/traits/miner.rs @@ -130,6 +130,9 @@ pub trait GetMinerData: + GetWattage + GetTuningTarget + GetScaledTuningTarget + + GetDefaultPowerTarget + + GetMinPowerTarget + + GetMaxPowerTarget + GetLightFlashing + GetMessages + GetUptime @@ -188,6 +191,9 @@ impl< + GetWattage + GetTuningTarget + GetScaledTuningTarget + + GetDefaultPowerTarget + + GetMinPowerTarget + + GetMaxPowerTarget + GetLightFlashing + GetMessages + GetUptime @@ -224,6 +230,9 @@ impl< let wattage = self.parse_wattage(&data); let tuning_target = self.parse_tuning_target(&data); let scaled_tuning_target = self.parse_scaled_tuning_target(&data); + let default_power_target = self.parse_default_power_target(&data); + let min_power_target = self.parse_min_power_target(&data); + let max_power_target = self.parse_max_power_target(&data); let fluid_temperature = self.parse_fluid_temperature(&data); let outlet_fluid_temperature = self.parse_outlet_fluid_temperature(&data); let fans = self.parse_fans(&data); @@ -314,6 +323,9 @@ impl< wattage, tuning_target, scaled_tuning_target, + default_power_target, + min_power_target, + max_power_target, efficiency, // Status information @@ -651,6 +663,54 @@ pub trait GetScaledTuningTarget: CollectData { } } +// Default Power Target +#[async_trait] +pub trait GetDefaultPowerTarget: CollectData { + #[tracing::instrument(level = "debug")] + async fn get_default_power_target(&self) -> Option { + let mut collector = self.get_collector(); + let data = collector.collect(&[DataField::DefaultPowerTarget]).await; + self.parse_default_power_target(&data) + } + + #[allow(unused_variables)] + fn parse_default_power_target(&self, data: &HashMap) -> Option { + None + } +} + +// Min Power Target +#[async_trait] +pub trait GetMinPowerTarget: CollectData { + #[tracing::instrument(level = "debug")] + async fn get_min_power_target(&self) -> Option { + let mut collector = self.get_collector(); + let data = collector.collect(&[DataField::MinPowerTarget]).await; + self.parse_min_power_target(&data) + } + + #[allow(unused_variables)] + fn parse_min_power_target(&self, data: &HashMap) -> Option { + None + } +} + +// Max Power Target +#[async_trait] +pub trait GetMaxPowerTarget: CollectData { + #[tracing::instrument(level = "debug")] + async fn get_max_power_target(&self) -> Option { + let mut collector = self.get_collector(); + let data = collector.collect(&[DataField::MaxPowerTarget]).await; + self.parse_max_power_target(&data) + } + + #[allow(unused_variables)] + fn parse_max_power_target(&self, data: &HashMap) -> Option { + None + } +} + // Light Flashing #[async_trait] pub trait GetLightFlashing: CollectData { diff --git a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs index 93c2d7b0..15d9d228 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs @@ -889,6 +889,9 @@ impl GetFluidTemperature for AntMinerV2020 { } impl GetPsuFans for AntMinerV2020 {} +impl GetDefaultPowerTarget for AntMinerV2020 {} +impl GetMinPowerTarget for AntMinerV2020 {} +impl GetMaxPowerTarget for AntMinerV2020 {} impl GetMessages for AntMinerV2020 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs index 3f663cab..12d70fd5 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs @@ -792,6 +792,9 @@ impl GetFluidTemperature for AntMinerV202307 { } impl GetPsuFans for AntMinerV202307 {} +impl GetDefaultPowerTarget for AntMinerV202307 {} +impl GetMinPowerTarget for AntMinerV202307 {} +impl GetMaxPowerTarget for AntMinerV202307 {} impl GetMessages for AntMinerV202307 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs index a97a21a9..1e54a6c7 100644 --- a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs @@ -1118,7 +1118,9 @@ impl GetTuningTarget for AuradineV1 { } impl GetScaledTuningTarget for AuradineV1 {} - +impl GetDefaultPowerTarget for AuradineV1 {} +impl GetMinPowerTarget for AuradineV1 {} +impl GetMaxPowerTarget for AuradineV1 {} impl GetLightFlashing for AuradineV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |code| code == 3) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs index 2db3eb8f..422f6d70 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs @@ -683,7 +683,9 @@ impl GetTuningTarget for AvalonAMiner { } impl GetScaledTuningTarget for AvalonAMiner {} - +impl GetDefaultPowerTarget for AvalonAMiner {} +impl GetMinPowerTarget for AvalonAMiner {} +impl GetMaxPowerTarget for AvalonAMiner {} impl GetLightFlashing for AvalonAMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs index d41c2d20..34c7fa49 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs @@ -627,7 +627,9 @@ impl GetTuningTarget for AvalonQMiner { } impl GetScaledTuningTarget for AvalonQMiner {} - +impl GetDefaultPowerTarget for AvalonQMiner {} +impl GetMinPowerTarget for AvalonQMiner {} +impl GetMaxPowerTarget for AvalonQMiner {} impl GetLightFlashing for AvalonQMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs index 032a2eb9..179a845e 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs @@ -386,6 +386,9 @@ impl GetTuningTarget for Bitaxe200 { impl GetScaledTuningTarget for Bitaxe200 { // N/A } +impl GetDefaultPowerTarget for Bitaxe200 {} +impl GetMinPowerTarget for Bitaxe200 {} +impl GetMaxPowerTarget for Bitaxe200 {} impl GetLightFlashing for Bitaxe200 { // N/A } diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs index e229eca5..a0b1deba 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs @@ -371,6 +371,9 @@ impl GetTuningTarget for Bitaxe290 { impl GetScaledTuningTarget for Bitaxe290 { // N/A } +impl GetDefaultPowerTarget for Bitaxe290 {} +impl GetMinPowerTarget for Bitaxe290 {} +impl GetMaxPowerTarget for Bitaxe290 {} impl GetLightFlashing for Bitaxe290 { // N/A } diff --git a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs index 3906596f..05de90c4 100644 --- a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs @@ -394,6 +394,9 @@ impl GetFirmwareVersion for BraiinsV2109 { } impl GetControlBoardVersion for BraiinsV2109 {} +impl GetDefaultPowerTarget for BraiinsV2109 {} +impl GetMinPowerTarget for BraiinsV2109 {} +impl GetMaxPowerTarget for BraiinsV2109 {} impl GetHashboards for BraiinsV2109 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs index b4bfb047..4c094f2e 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs @@ -389,6 +389,9 @@ impl GetFirmwareVersion for BraiinsV2503 { } impl GetControlBoardVersion for BraiinsV2503 {} +impl GetDefaultPowerTarget for BraiinsV2503 {} +impl GetMinPowerTarget for BraiinsV2503 {} +impl GetMaxPowerTarget for BraiinsV2503 {} impl GetHashboards for BraiinsV2503 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs index 5fc487c1..f9d89616 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs @@ -390,6 +390,9 @@ impl GetFirmwareVersion for BraiinsV2505 { } impl GetControlBoardVersion for BraiinsV2505 {} +impl GetDefaultPowerTarget for BraiinsV2505 {} +impl GetMinPowerTarget for BraiinsV2505 {} +impl GetMaxPowerTarget for BraiinsV2505 {} impl GetHashboards for BraiinsV2505 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs index d4e04c9b..22ead517 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs @@ -610,6 +610,9 @@ impl GetScaledTuningTarget for BraiinsV2507 { } impl GetFluidTemperature for BraiinsV2507 {} +impl GetDefaultPowerTarget for BraiinsV2507 {} +impl GetMinPowerTarget for BraiinsV2507 {} +impl GetMaxPowerTarget for BraiinsV2507 {} impl GetPsuFans for BraiinsV2507 {} diff --git a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs index b3eff350..5cacc48d 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -139,6 +139,21 @@ impl GetDataLocations for BraiinsV2604 { } }"#, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::Mac => vec![( @@ -313,6 +328,30 @@ impl GetDataLocations for BraiinsV2604 { tag: None, }, )], + DataField::DefaultPowerTarget => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget/default"), + tag: None, + }, + )], + DataField::MinPowerTarget => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget/min"), + tag: None, + }, + )], + DataField::MaxPowerTarget => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget/max"), + tag: None, + }, + )], _ => vec![], } } @@ -606,6 +645,21 @@ impl GetScaledTuningTarget for BraiinsV2604 { } impl GetFluidTemperature for BraiinsV2604 {} +impl GetDefaultPowerTarget for BraiinsV2604 { + fn parse_default_power_target(&self, data: &HashMap) -> Option { + data.extract_map::(DataField::DefaultPowerTarget, |w| Power::from_watts(w as f64)) + } +} +impl GetMinPowerTarget for BraiinsV2604 { + fn parse_min_power_target(&self, data: &HashMap) -> Option { + data.extract_map::(DataField::MinPowerTarget, |w| Power::from_watts(w as f64)) + } +} +impl GetMaxPowerTarget for BraiinsV2604 { + fn parse_max_power_target(&self, data: &HashMap) -> Option { + data.extract_map::(DataField::MaxPowerTarget, |w| Power::from_watts(w as f64)) + } +} impl GetPsuFans for BraiinsV2604 {} diff --git a/asic-rs-firmwares/epic/src/backends/v1/mod.rs b/asic-rs-firmwares/epic/src/backends/v1/mod.rs index bfff0283..41b75259 100644 --- a/asic-rs-firmwares/epic/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/epic/src/backends/v1/mod.rs @@ -835,6 +835,9 @@ impl GetFans for PowerPlayV1 { } impl GetPsuFans for PowerPlayV1 {} +impl GetDefaultPowerTarget for PowerPlayV1 {} +impl GetMinPowerTarget for PowerPlayV1 {} +impl GetMaxPowerTarget for PowerPlayV1 {} impl GetFluidTemperature for PowerPlayV1 {} diff --git a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs index 66f0f59f..f16cfe39 100644 --- a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs @@ -600,7 +600,9 @@ impl GetWattage for ApolloV2 { impl GetTuningTarget for ApolloV2 {} impl GetScaledTuningTarget for ApolloV2 {} - +impl GetDefaultPowerTarget for ApolloV2 {} +impl GetMinPowerTarget for ApolloV2 {} +impl GetMaxPowerTarget for ApolloV2 {} impl GetLightFlashing for ApolloV2 {} impl GetMessages for ApolloV2 {} diff --git a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs index d7637298..79c68e76 100644 --- a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs @@ -984,7 +984,9 @@ impl GetTuningTarget for LuxMinerV1 { } impl GetScaledTuningTarget for LuxMinerV1 {} - +impl GetDefaultPowerTarget for LuxMinerV1 {} +impl GetMinPowerTarget for LuxMinerV1 {} +impl GetMaxPowerTarget for LuxMinerV1 {} impl GetPsuFans for LuxMinerV1 {} impl GetMessages for LuxMinerV1 { diff --git a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs index ef2ec29f..30d47c21 100644 --- a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs @@ -884,7 +884,9 @@ impl GetTuningTarget for MaraV1 { } impl GetScaledTuningTarget for MaraV1 {} - +impl GetDefaultPowerTarget for MaraV1 {} +impl GetMinPowerTarget for MaraV1 {} +impl GetMaxPowerTarget for MaraV1 {} impl GetLightFlashing for MaraV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs index ee02b28e..53e40b68 100644 --- a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs @@ -375,6 +375,9 @@ impl GetWattage for NerdAxeV1 { } impl GetTuningTarget for NerdAxeV1 {} impl GetScaledTuningTarget for NerdAxeV1 {} +impl GetDefaultPowerTarget for NerdAxeV1 {} +impl GetMinPowerTarget for NerdAxeV1 {} +impl GetMaxPowerTarget for NerdAxeV1 {} impl GetLightFlashing for NerdAxeV1 {} impl GetMessages for NerdAxeV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/proto/src/backends/v1/mod.rs b/asic-rs-firmwares/proto/src/backends/v1/mod.rs index 814ba6ee..916a1cac 100644 --- a/asic-rs-firmwares/proto/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/proto/src/backends/v1/mod.rs @@ -623,7 +623,9 @@ impl GetTuningTarget for ProtoV1 { } impl GetScaledTuningTarget for ProtoV1 {} - +impl GetDefaultPowerTarget for ProtoV1 {} +impl GetMinPowerTarget for ProtoV1 {} +impl GetMaxPowerTarget for ProtoV1 {} // No endpoint reports locate-LED state, so it's set-only. impl GetLightFlashing for ProtoV1 {} diff --git a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs index 13ea0323..df95a0af 100644 --- a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs +++ b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs @@ -553,7 +553,9 @@ impl GetTuningTarget for SealMinerV2025 { } impl GetScaledTuningTarget for SealMinerV2025 {} - +impl GetDefaultPowerTarget for SealMinerV2025 {} +impl GetMinPowerTarget for SealMinerV2025 {} +impl GetMaxPowerTarget for SealMinerV2025 {} impl GetLightFlashing for SealMinerV2025 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs index 1b914e1e..3a1b8b44 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs @@ -592,7 +592,9 @@ impl GetWattage for VnishV120 { impl GetTuningTarget for VnishV120 {} impl GetScaledTuningTarget for VnishV120 {} - +impl GetDefaultPowerTarget for VnishV120 {} +impl GetMinPowerTarget for VnishV120 {} +impl GetMaxPowerTarget for VnishV120 {} impl GetLightFlashing for VnishV120 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs index 3be418c2..d5d6f308 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs @@ -435,6 +435,9 @@ impl GetTuningTarget for WhatsMinerV1 { } } impl GetScaledTuningTarget for WhatsMinerV1 {} +impl GetDefaultPowerTarget for WhatsMinerV1 {} +impl GetMinPowerTarget for WhatsMinerV1 {} +impl GetMaxPowerTarget for WhatsMinerV1 {} impl GetLightFlashing for WhatsMinerV1 {} impl GetMessages for WhatsMinerV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs index f63911c1..1c69aade 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs @@ -499,6 +499,9 @@ impl GetTuningTarget for WhatsMinerV2 { } } impl GetScaledTuningTarget for WhatsMinerV2 {} +impl GetDefaultPowerTarget for WhatsMinerV2 {} +impl GetMinPowerTarget for WhatsMinerV2 {} +impl GetMaxPowerTarget for WhatsMinerV2 {} impl GetLightFlashing for WhatsMinerV2 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs index 02f4ad05..76b8fc07 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs @@ -534,6 +534,9 @@ impl GetTuningTarget for WhatsMinerV3 { } } impl GetScaledTuningTarget for WhatsMinerV3 {} +impl GetDefaultPowerTarget for WhatsMinerV3 {} +impl GetMinPowerTarget for WhatsMinerV3 {} +impl GetMaxPowerTarget for WhatsMinerV3 {} impl GetLightFlashing for WhatsMinerV3 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/python/pyasic_rs/asic_rs.pyi b/python/pyasic_rs/asic_rs.pyi index e9bfb588..474f24eb 100644 --- a/python/pyasic_rs/asic_rs.pyi +++ b/python/pyasic_rs/asic_rs.pyi @@ -82,6 +82,7 @@ class DataField: AverageTemperature: Final[DataField] Chips: Final[DataField] ControlBoardVersion: Final[DataField] + DefaultPowerTarget: Final[DataField] DeviceInfo: Final[DataField] Efficiency: Final[DataField] ExpectedHashrate: Final[DataField] @@ -95,7 +96,9 @@ class DataField: IsMining: Final[DataField] LightFlashing: Final[DataField] Mac: Final[DataField] + MaxPowerTarget: Final[DataField] Messages: Final[DataField] + MinPowerTarget: Final[DataField] OutletFluidTemperature: Final[DataField] Pools: Final[DataField] PsuFans: Final[DataField] @@ -495,6 +498,8 @@ class MinerData: @property def control_board_version(self, /) -> MinerControlBoard |None: ... @property + def default_power_target(self, /) -> float |None: ... + @property def device_info(self, /) -> DeviceInfo: ... @property def efficiency(self, /) -> float |None: ... @@ -527,7 +532,11 @@ class MinerData: @property def mac(self, /) -> str |None: ... @property + def max_power_target(self, /) -> float |None: ... + @property def messages(self, /) -> list[MinerMessage]: ... + @property + def min_power_target(self, /) -> float |None: ... def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... @classmethod def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... diff --git a/python/tests/test_pydantic_interop.py b/python/tests/test_pydantic_interop.py index 9265be90..eac5d2da 100644 --- a/python/tests/test_pydantic_interop.py +++ b/python/tests/test_pydantic_interop.py @@ -119,6 +119,9 @@ def minimal_miner_data(**overrides: object) -> dict[str, object]: "wattage": None, "tuning_target": None, "scaled_tuning_target": None, + "default_power_target": None, + "min_power_target": None, + "max_power_target": None, "efficiency": None, "light_flashing": None, "messages": [], From 2657801124f78b26c4da2846b8a305d94c8d91be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:49:54 +0000 Subject: [PATCH 2/4] chore: format --- asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs index 5cacc48d..15ec69db 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -647,7 +647,9 @@ impl GetScaledTuningTarget for BraiinsV2604 { impl GetFluidTemperature for BraiinsV2604 {} impl GetDefaultPowerTarget for BraiinsV2604 { fn parse_default_power_target(&self, data: &HashMap) -> Option { - data.extract_map::(DataField::DefaultPowerTarget, |w| Power::from_watts(w as f64)) + data.extract_map::(DataField::DefaultPowerTarget, |w| { + Power::from_watts(w as f64) + }) } } impl GetMinPowerTarget for BraiinsV2604 { From c887451607dbd7df8baa6767148bd4efeeb3dde3 Mon Sep 17 00:00:00 2001 From: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:30:51 +0000 Subject: [PATCH 3/4] refactor(capabilities): model tuning envelope as TuningCapabilities Per review feedback, replace the flat default/min/max power-target fields with a structured MinerCapabilities-style TuningCapabilities, grouped by tuning domain (power / hashrate / presets), each reusing the existing TuningTarget: TuningCapabilities { power: Option, // default / minimum / maximum hashrate: Option, // default / minimum / maximum presets: Option, // default / presets } - new asic-rs-core/src/data/capabilities.rs with the four structs (pydantic- exposed like BoardData) - single DataField::TuningCapabilities + GetTuningCapabilities trait replace the three power-only fields/traits - MinerData.tuning_capabilities replaces default/min/max_power_target - BraiinsV2604 fills power.{default,minimum,maximum} from the autotuning powerTarget metadata; hashrate/presets left for the backends that expose them - python bindings (.pyi, data.py, module export) + interop test updated --- asic-rs-core/src/data/capabilities.rs | 67 +++++++++++++ asic-rs-core/src/data/collector.rs | 13 +-- asic-rs-core/src/data/miner.rs | 14 +-- asic-rs-core/src/data/mod.rs | 1 + asic-rs-core/src/traits/miner.rs | 64 +++---------- .../antminer/src/backends/v2020/mod.rs | 4 +- .../antminer/src/backends/v2023_07/mod.rs | 4 +- .../auradine/src/backends/v1/mod.rs | 4 +- .../avalonminer/src/backends/avalon_a/mod.rs | 4 +- .../avalonminer/src/backends/avalon_q/mod.rs | 4 +- .../bitaxe/src/backends/v2_0_0/mod.rs | 4 +- .../bitaxe/src/backends/v2_9_0/mod.rs | 4 +- .../braiins/src/backends/v21_09/mod.rs | 4 +- .../braiins/src/backends/v25_03/mod.rs | 4 +- .../braiins/src/backends/v25_05/mod.rs | 4 +- .../braiins/src/backends/v25_07/mod.rs | 4 +- .../braiins/src/backends/v26_04/mod.rs | 60 ++++++------ asic-rs-firmwares/epic/src/backends/v1/mod.rs | 4 +- .../futurebit/src/backends/v2/mod.rs | 4 +- .../luxminer/src/backends/v1/mod.rs | 4 +- .../marathon/src/backends/v1/mod.rs | 4 +- .../nerdaxe/src/backends/v1/mod.rs | 4 +- .../proto/src/backends/v1/mod.rs | 4 +- .../sealminer/src/backends/v2025/mod.rs | 4 +- .../vnish/src/backends/v1_2_0/mod.rs | 4 +- .../whatsminer/src/backends/v1/mod.rs | 4 +- .../whatsminer/src/backends/v2/mod.rs | 4 +- .../whatsminer/src/backends/v3/mod.rs | 4 +- python/pyasic_rs/asic_rs.pyi | 94 +++++++++++++++++-- python/pyasic_rs/data.py | 10 ++ python/tests/test_pydantic_interop.py | 4 +- src/python/mod.rs | 4 + 32 files changed, 240 insertions(+), 179 deletions(-) create mode 100644 asic-rs-core/src/data/capabilities.rs diff --git a/asic-rs-core/src/data/capabilities.rs b/asic-rs-core/src/data/capabilities.rs new file mode 100644 index 00000000..5c7eaaa6 --- /dev/null +++ b/asic-rs-core/src/data/capabilities.rs @@ -0,0 +1,67 @@ +//! Tuning capability metadata reported by a miner's firmware. +//! +//! Where [`MinerData::tuning_target`](super::miner::MinerData::tuning_target) +//! describes the *current* tuning setpoint, these types describe the *envelope* +//! the firmware allows: the factory default plus the accepted range (for power +//! and hashrate tuning) or the set of selectable presets. Every field is +//! optional — a backend fills in only what its firmware actually exposes. + +#[cfg(feature = "python")] +use pyo3::pyclass; +use serde::{Deserialize, Serialize}; + +use super::miner::TuningTarget; + +/// Factory tuning envelope for power-target tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct PowerTuningCapabilities { + /// The factory default power target, i.e. the value tuning starts from. + pub default: Option, + /// The lowest power target the miner accepts. + pub minimum: Option, + /// The highest power target the miner accepts. + pub maximum: Option, +} + +/// Factory tuning envelope for hashrate-target tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct HashRateTuningCapabilities { + /// The factory default hashrate target, i.e. the value tuning starts from. + pub default: Option, + /// The lowest hashrate target the miner accepts. + pub minimum: Option, + /// The highest hashrate target the miner accepts. + pub maximum: Option, +} + +/// Factory tuning envelope for preset / mining-mode tuning. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct PresetTuningCapabilities { + /// The factory default preset, when one is marked as default. + pub default: Option, + /// The presets the firmware offers to select between. + pub presets: Vec, +} + +/// The tuning envelopes a miner exposes, grouped by tuning domain. +/// +/// A backend populates only the domains its firmware supports; the rest stay +/// `None`. This keeps a single, extensible home for "what can this miner be +/// tuned to" instead of a flat list of per-aspect fields. +#[cfg_attr(feature = "python", pyclass(from_py_object, module = "asic_rs"))] +#[cfg_attr(feature = "python", asic_rs_pydantic::py_pydantic_model(getters))] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct TuningCapabilities { + /// Power-target tuning envelope, when the firmware tunes by power. + pub power: Option, + /// Hashrate-target tuning envelope, when the firmware tunes by hashrate. + pub hashrate: Option, + /// Preset / mining-mode tuning envelope, when the firmware tunes by preset. + pub presets: Option, +} diff --git a/asic-rs-core/src/data/collector.rs b/asic-rs-core/src/data/collector.rs index cc8bf50a..971b68ca 100644 --- a/asic-rs-core/src/data/collector.rs +++ b/asic-rs-core/src/data/collector.rs @@ -77,15 +77,10 @@ pub enum DataField { /// Configured tuning target (power or hashrate). #[cfg_attr(feature = "python", pydantic(value = "TuningTarget"))] TuningTarget, - /// Factory default power target the miner ships with (the value all tuning starts from). - #[cfg_attr(feature = "python", pydantic(value = "DefaultPowerTarget"))] - DefaultPowerTarget, - /// Lowest power target the miner accepts. - #[cfg_attr(feature = "python", pydantic(value = "MinPowerTarget"))] - MinPowerTarget, - /// Highest power target the miner accepts. - #[cfg_attr(feature = "python", pydantic(value = "MaxPowerTarget"))] - MaxPowerTarget, + /// Factory tuning envelope the firmware exposes (default/min/max power or + /// hashrate, or selectable presets). + #[cfg_attr(feature = "python", pydantic(value = "TuningCapabilities"))] + TuningCapabilities, /// Efficiency of the miner (e.g., J/TH). #[cfg_attr(feature = "python", pydantic(value = "Efficiency"))] Efficiency, diff --git a/asic-rs-core/src/data/miner.rs b/asic-rs-core/src/data/miner.rs index 56f22d75..9ee31953 100644 --- a/asic-rs-core/src/data/miner.rs +++ b/asic-rs-core/src/data/miner.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use super::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, device::DeviceInfo, fan::FanData, hashrate::HashRate, @@ -118,16 +119,9 @@ pub struct MinerData { pub tuning_target: Option, /// The current tuning target adjusted by scaling settings, when available. pub scaled_tuning_target: Option, - /// The factory default power target the miner ships with, i.e. the value all - /// tuning starts from. Reported by firmwares that expose tuner metadata. - #[serde(serialize_with = "serialize_power")] - pub default_power_target: Option, - /// The lowest power target the miner accepts, when reported. - #[serde(serialize_with = "serialize_power")] - pub min_power_target: Option, - /// The highest power target the miner accepts, when reported. - #[serde(serialize_with = "serialize_power")] - pub max_power_target: Option, + /// The factory tuning envelope the firmware exposes (default / min / max + /// power and hashrate, or selectable presets), when reported. + pub tuning_capabilities: Option, /// The current efficiency in W/TH/s (J/TH) of the miner pub efficiency: Option, /// The state of the fault/alert light on the miner diff --git a/asic-rs-core/src/data/mod.rs b/asic-rs-core/src/data/mod.rs index 0d147914..889afc5e 100644 --- a/asic-rs-core/src/data/mod.rs +++ b/asic-rs-core/src/data/mod.rs @@ -3,6 +3,7 @@ //! The most important data type is [`MinerData`][`miner::MinerData`], it contains all the data asic-rs gathers with `get_data`. pub mod board; +pub mod capabilities; pub mod collector; pub mod command; pub(crate) mod deserialize; diff --git a/asic-rs-core/src/traits/miner.rs b/asic-rs-core/src/traits/miner.rs index 510878b8..7bdf4804 100644 --- a/asic-rs-core/src/traits/miner.rs +++ b/asic-rs-core/src/traits/miner.rs @@ -19,6 +19,7 @@ use crate::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, collector::{DataCollector, DataField, DataLocation}, command::MinerCommand, device::DeviceInfo, @@ -130,9 +131,7 @@ pub trait GetMinerData: + GetWattage + GetTuningTarget + GetScaledTuningTarget - + GetDefaultPowerTarget - + GetMinPowerTarget - + GetMaxPowerTarget + + GetTuningCapabilities + GetLightFlashing + GetMessages + GetUptime @@ -191,9 +190,7 @@ impl< + GetWattage + GetTuningTarget + GetScaledTuningTarget - + GetDefaultPowerTarget - + GetMinPowerTarget - + GetMaxPowerTarget + + GetTuningCapabilities + GetLightFlashing + GetMessages + GetUptime @@ -230,9 +227,7 @@ impl< let wattage = self.parse_wattage(&data); let tuning_target = self.parse_tuning_target(&data); let scaled_tuning_target = self.parse_scaled_tuning_target(&data); - let default_power_target = self.parse_default_power_target(&data); - let min_power_target = self.parse_min_power_target(&data); - let max_power_target = self.parse_max_power_target(&data); + let tuning_capabilities = self.parse_tuning_capabilities(&data); let fluid_temperature = self.parse_fluid_temperature(&data); let outlet_fluid_temperature = self.parse_outlet_fluid_temperature(&data); let fans = self.parse_fans(&data); @@ -323,9 +318,7 @@ impl< wattage, tuning_target, scaled_tuning_target, - default_power_target, - min_power_target, - max_power_target, + tuning_capabilities, efficiency, // Status information @@ -663,50 +656,21 @@ pub trait GetScaledTuningTarget: CollectData { } } -// Default Power Target +// Tuning Capabilities #[async_trait] -pub trait GetDefaultPowerTarget: CollectData { +pub trait GetTuningCapabilities: CollectData { #[tracing::instrument(level = "debug")] - async fn get_default_power_target(&self) -> Option { + async fn get_tuning_capabilities(&self) -> Option { let mut collector = self.get_collector(); - let data = collector.collect(&[DataField::DefaultPowerTarget]).await; - self.parse_default_power_target(&data) + let data = collector.collect(&[DataField::TuningCapabilities]).await; + self.parse_tuning_capabilities(&data) } #[allow(unused_variables)] - fn parse_default_power_target(&self, data: &HashMap) -> Option { - None - } -} - -// Min Power Target -#[async_trait] -pub trait GetMinPowerTarget: CollectData { - #[tracing::instrument(level = "debug")] - async fn get_min_power_target(&self) -> Option { - let mut collector = self.get_collector(); - let data = collector.collect(&[DataField::MinPowerTarget]).await; - self.parse_min_power_target(&data) - } - - #[allow(unused_variables)] - fn parse_min_power_target(&self, data: &HashMap) -> Option { - None - } -} - -// Max Power Target -#[async_trait] -pub trait GetMaxPowerTarget: CollectData { - #[tracing::instrument(level = "debug")] - async fn get_max_power_target(&self) -> Option { - let mut collector = self.get_collector(); - let data = collector.collect(&[DataField::MaxPowerTarget]).await; - self.parse_max_power_target(&data) - } - - #[allow(unused_variables)] - fn parse_max_power_target(&self, data: &HashMap) -> Option { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { None } } diff --git a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs index 15d9d228..bbd0ec3e 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2020/mod.rs @@ -889,9 +889,7 @@ impl GetFluidTemperature for AntMinerV2020 { } impl GetPsuFans for AntMinerV2020 {} -impl GetDefaultPowerTarget for AntMinerV2020 {} -impl GetMinPowerTarget for AntMinerV2020 {} -impl GetMaxPowerTarget for AntMinerV2020 {} +impl GetTuningCapabilities for AntMinerV2020 {} impl GetMessages for AntMinerV2020 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs index 12d70fd5..b0e1e926 100644 --- a/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs +++ b/asic-rs-firmwares/antminer/src/backends/v2023_07/mod.rs @@ -792,9 +792,7 @@ impl GetFluidTemperature for AntMinerV202307 { } impl GetPsuFans for AntMinerV202307 {} -impl GetDefaultPowerTarget for AntMinerV202307 {} -impl GetMinPowerTarget for AntMinerV202307 {} -impl GetMaxPowerTarget for AntMinerV202307 {} +impl GetTuningCapabilities for AntMinerV202307 {} impl GetMessages for AntMinerV202307 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs index 1e54a6c7..8dc1d2f7 100644 --- a/asic-rs-firmwares/auradine/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/auradine/src/backends/v1/mod.rs @@ -1118,9 +1118,7 @@ impl GetTuningTarget for AuradineV1 { } impl GetScaledTuningTarget for AuradineV1 {} -impl GetDefaultPowerTarget for AuradineV1 {} -impl GetMinPowerTarget for AuradineV1 {} -impl GetMaxPowerTarget for AuradineV1 {} +impl GetTuningCapabilities for AuradineV1 {} impl GetLightFlashing for AuradineV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |code| code == 3) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs index 422f6d70..d78c0f75 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_a/mod.rs @@ -683,9 +683,7 @@ impl GetTuningTarget for AvalonAMiner { } impl GetScaledTuningTarget for AvalonAMiner {} -impl GetDefaultPowerTarget for AvalonAMiner {} -impl GetMinPowerTarget for AvalonAMiner {} -impl GetMaxPowerTarget for AvalonAMiner {} +impl GetTuningCapabilities for AvalonAMiner {} impl GetLightFlashing for AvalonAMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs index 34c7fa49..3548f3cd 100644 --- a/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs +++ b/asic-rs-firmwares/avalonminer/src/backends/avalon_q/mod.rs @@ -627,9 +627,7 @@ impl GetTuningTarget for AvalonQMiner { } impl GetScaledTuningTarget for AvalonQMiner {} -impl GetDefaultPowerTarget for AvalonQMiner {} -impl GetMinPowerTarget for AvalonQMiner {} -impl GetMaxPowerTarget for AvalonQMiner {} +impl GetTuningCapabilities for AvalonQMiner {} impl GetLightFlashing for AvalonQMiner { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs index 179a845e..07cc0597 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_0_0/mod.rs @@ -386,9 +386,7 @@ impl GetTuningTarget for Bitaxe200 { impl GetScaledTuningTarget for Bitaxe200 { // N/A } -impl GetDefaultPowerTarget for Bitaxe200 {} -impl GetMinPowerTarget for Bitaxe200 {} -impl GetMaxPowerTarget for Bitaxe200 {} +impl GetTuningCapabilities for Bitaxe200 {} impl GetLightFlashing for Bitaxe200 { // N/A } diff --git a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs index a0b1deba..02502930 100644 --- a/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs +++ b/asic-rs-firmwares/bitaxe/src/backends/v2_9_0/mod.rs @@ -371,9 +371,7 @@ impl GetTuningTarget for Bitaxe290 { impl GetScaledTuningTarget for Bitaxe290 { // N/A } -impl GetDefaultPowerTarget for Bitaxe290 {} -impl GetMinPowerTarget for Bitaxe290 {} -impl GetMaxPowerTarget for Bitaxe290 {} +impl GetTuningCapabilities for Bitaxe290 {} impl GetLightFlashing for Bitaxe290 { // N/A } diff --git a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs index 05de90c4..f29ca012 100644 --- a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs @@ -394,9 +394,7 @@ impl GetFirmwareVersion for BraiinsV2109 { } impl GetControlBoardVersion for BraiinsV2109 {} -impl GetDefaultPowerTarget for BraiinsV2109 {} -impl GetMinPowerTarget for BraiinsV2109 {} -impl GetMaxPowerTarget for BraiinsV2109 {} +impl GetTuningCapabilities for BraiinsV2109 {} impl GetHashboards for BraiinsV2109 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs index 4c094f2e..810bd3ee 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs @@ -389,9 +389,7 @@ impl GetFirmwareVersion for BraiinsV2503 { } impl GetControlBoardVersion for BraiinsV2503 {} -impl GetDefaultPowerTarget for BraiinsV2503 {} -impl GetMinPowerTarget for BraiinsV2503 {} -impl GetMaxPowerTarget for BraiinsV2503 {} +impl GetTuningCapabilities for BraiinsV2503 {} impl GetHashboards for BraiinsV2503 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs index f9d89616..d562b06e 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs @@ -390,9 +390,7 @@ impl GetFirmwareVersion for BraiinsV2505 { } impl GetControlBoardVersion for BraiinsV2505 {} -impl GetDefaultPowerTarget for BraiinsV2505 {} -impl GetMinPowerTarget for BraiinsV2505 {} -impl GetMaxPowerTarget for BraiinsV2505 {} +impl GetTuningCapabilities for BraiinsV2505 {} impl GetHashboards for BraiinsV2505 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs index 22ead517..67b5f025 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs @@ -610,9 +610,7 @@ impl GetScaledTuningTarget for BraiinsV2507 { } impl GetFluidTemperature for BraiinsV2507 {} -impl GetDefaultPowerTarget for BraiinsV2507 {} -impl GetMinPowerTarget for BraiinsV2507 {} -impl GetMaxPowerTarget for BraiinsV2507 {} +impl GetTuningCapabilities for BraiinsV2507 {} impl GetPsuFans for BraiinsV2507 {} diff --git a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs index 15ec69db..687151cc 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::{PowerTuningCapabilities, TuningCapabilities}, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -328,27 +329,11 @@ impl GetDataLocations for BraiinsV2604 { tag: None, }, )], - DataField::DefaultPowerTarget => vec![( + DataField::TuningCapabilities => vec![( GQL_POWER_TARGET_META_QUERY, DataExtractor { func: get_by_pointer, - key: Some("/bosminer/metadata/autotuning/powerTarget/default"), - tag: None, - }, - )], - DataField::MinPowerTarget => vec![( - GQL_POWER_TARGET_META_QUERY, - DataExtractor { - func: get_by_pointer, - key: Some("/bosminer/metadata/autotuning/powerTarget/min"), - tag: None, - }, - )], - DataField::MaxPowerTarget => vec![( - GQL_POWER_TARGET_META_QUERY, - DataExtractor { - func: get_by_pointer, - key: Some("/bosminer/metadata/autotuning/powerTarget/max"), + key: Some("/bosminer/metadata/autotuning/powerTarget"), tag: None, }, )], @@ -645,23 +630,34 @@ impl GetScaledTuningTarget for BraiinsV2604 { } impl GetFluidTemperature for BraiinsV2604 {} -impl GetDefaultPowerTarget for BraiinsV2604 { - fn parse_default_power_target(&self, data: &HashMap) -> Option { - data.extract_map::(DataField::DefaultPowerTarget, |w| { - Power::from_watts(w as f64) +impl GetTuningCapabilities for BraiinsV2604 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + // The extractor stores the `powerTarget` object ({default, min, max}) in + // watts under the field; read the three watt values from it. + let power_target = data.get(&DataField::TuningCapabilities)?; + let watts = |key: &str| { + power_target + .get(key) + .and_then(|v| v.as_i64()) + .map(|w| TuningTarget::from_watts(w as f64)) + }; + let power = PowerTuningCapabilities { + default: watts("default"), + minimum: watts("min"), + maximum: watts("max"), + }; + if power == PowerTuningCapabilities::default() { + return None; + } + Some(TuningCapabilities { + power: Some(power), + ..Default::default() }) } } -impl GetMinPowerTarget for BraiinsV2604 { - fn parse_min_power_target(&self, data: &HashMap) -> Option { - data.extract_map::(DataField::MinPowerTarget, |w| Power::from_watts(w as f64)) - } -} -impl GetMaxPowerTarget for BraiinsV2604 { - fn parse_max_power_target(&self, data: &HashMap) -> Option { - data.extract_map::(DataField::MaxPowerTarget, |w| Power::from_watts(w as f64)) - } -} impl GetPsuFans for BraiinsV2604 {} diff --git a/asic-rs-firmwares/epic/src/backends/v1/mod.rs b/asic-rs-firmwares/epic/src/backends/v1/mod.rs index 41b75259..d7a11409 100644 --- a/asic-rs-firmwares/epic/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/epic/src/backends/v1/mod.rs @@ -835,9 +835,7 @@ impl GetFans for PowerPlayV1 { } impl GetPsuFans for PowerPlayV1 {} -impl GetDefaultPowerTarget for PowerPlayV1 {} -impl GetMinPowerTarget for PowerPlayV1 {} -impl GetMaxPowerTarget for PowerPlayV1 {} +impl GetTuningCapabilities for PowerPlayV1 {} impl GetFluidTemperature for PowerPlayV1 {} diff --git a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs index f16cfe39..fb02b898 100644 --- a/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/futurebit/src/backends/v2/mod.rs @@ -600,9 +600,7 @@ impl GetWattage for ApolloV2 { impl GetTuningTarget for ApolloV2 {} impl GetScaledTuningTarget for ApolloV2 {} -impl GetDefaultPowerTarget for ApolloV2 {} -impl GetMinPowerTarget for ApolloV2 {} -impl GetMaxPowerTarget for ApolloV2 {} +impl GetTuningCapabilities for ApolloV2 {} impl GetLightFlashing for ApolloV2 {} impl GetMessages for ApolloV2 {} diff --git a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs index 79c68e76..e439234e 100644 --- a/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/luxminer/src/backends/v1/mod.rs @@ -984,9 +984,7 @@ impl GetTuningTarget for LuxMinerV1 { } impl GetScaledTuningTarget for LuxMinerV1 {} -impl GetDefaultPowerTarget for LuxMinerV1 {} -impl GetMinPowerTarget for LuxMinerV1 {} -impl GetMaxPowerTarget for LuxMinerV1 {} +impl GetTuningCapabilities for LuxMinerV1 {} impl GetPsuFans for LuxMinerV1 {} impl GetMessages for LuxMinerV1 { diff --git a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs index 30d47c21..10c236e4 100644 --- a/asic-rs-firmwares/marathon/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/marathon/src/backends/v1/mod.rs @@ -884,9 +884,7 @@ impl GetTuningTarget for MaraV1 { } impl GetScaledTuningTarget for MaraV1 {} -impl GetDefaultPowerTarget for MaraV1 {} -impl GetMinPowerTarget for MaraV1 {} -impl GetMaxPowerTarget for MaraV1 {} +impl GetTuningCapabilities for MaraV1 {} impl GetLightFlashing for MaraV1 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs index 53e40b68..df60e9d2 100644 --- a/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/nerdaxe/src/backends/v1/mod.rs @@ -375,9 +375,7 @@ impl GetWattage for NerdAxeV1 { } impl GetTuningTarget for NerdAxeV1 {} impl GetScaledTuningTarget for NerdAxeV1 {} -impl GetDefaultPowerTarget for NerdAxeV1 {} -impl GetMinPowerTarget for NerdAxeV1 {} -impl GetMaxPowerTarget for NerdAxeV1 {} +impl GetTuningCapabilities for NerdAxeV1 {} impl GetLightFlashing for NerdAxeV1 {} impl GetMessages for NerdAxeV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/proto/src/backends/v1/mod.rs b/asic-rs-firmwares/proto/src/backends/v1/mod.rs index 916a1cac..5fc1af1e 100644 --- a/asic-rs-firmwares/proto/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/proto/src/backends/v1/mod.rs @@ -623,9 +623,7 @@ impl GetTuningTarget for ProtoV1 { } impl GetScaledTuningTarget for ProtoV1 {} -impl GetDefaultPowerTarget for ProtoV1 {} -impl GetMinPowerTarget for ProtoV1 {} -impl GetMaxPowerTarget for ProtoV1 {} +impl GetTuningCapabilities for ProtoV1 {} // No endpoint reports locate-LED state, so it's set-only. impl GetLightFlashing for ProtoV1 {} diff --git a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs index df95a0af..7a38961b 100644 --- a/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs +++ b/asic-rs-firmwares/sealminer/src/backends/v2025/mod.rs @@ -553,9 +553,7 @@ impl GetTuningTarget for SealMinerV2025 { } impl GetScaledTuningTarget for SealMinerV2025 {} -impl GetDefaultPowerTarget for SealMinerV2025 {} -impl GetMinPowerTarget for SealMinerV2025 {} -impl GetMaxPowerTarget for SealMinerV2025 {} +impl GetTuningCapabilities for SealMinerV2025 {} impl GetLightFlashing for SealMinerV2025 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs index 3a1b8b44..10397e8e 100644 --- a/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs +++ b/asic-rs-firmwares/vnish/src/backends/v1_2_0/mod.rs @@ -592,9 +592,7 @@ impl GetWattage for VnishV120 { impl GetTuningTarget for VnishV120 {} impl GetScaledTuningTarget for VnishV120 {} -impl GetDefaultPowerTarget for VnishV120 {} -impl GetMinPowerTarget for VnishV120 {} -impl GetMaxPowerTarget for VnishV120 {} +impl GetTuningCapabilities for VnishV120 {} impl GetLightFlashing for VnishV120 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract::(DataField::LightFlashing) diff --git a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs index d5d6f308..c84f29c2 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v1/mod.rs @@ -435,9 +435,7 @@ impl GetTuningTarget for WhatsMinerV1 { } } impl GetScaledTuningTarget for WhatsMinerV1 {} -impl GetDefaultPowerTarget for WhatsMinerV1 {} -impl GetMinPowerTarget for WhatsMinerV1 {} -impl GetMaxPowerTarget for WhatsMinerV1 {} +impl GetTuningCapabilities for WhatsMinerV1 {} impl GetLightFlashing for WhatsMinerV1 {} impl GetMessages for WhatsMinerV1 { fn parse_messages(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs index 1c69aade..2aced011 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v2/mod.rs @@ -499,9 +499,7 @@ impl GetTuningTarget for WhatsMinerV2 { } } impl GetScaledTuningTarget for WhatsMinerV2 {} -impl GetDefaultPowerTarget for WhatsMinerV2 {} -impl GetMinPowerTarget for WhatsMinerV2 {} -impl GetMaxPowerTarget for WhatsMinerV2 {} +impl GetTuningCapabilities for WhatsMinerV2 {} impl GetLightFlashing for WhatsMinerV2 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs index 76b8fc07..db496e9d 100644 --- a/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs +++ b/asic-rs-firmwares/whatsminer/src/backends/v3/mod.rs @@ -534,9 +534,7 @@ impl GetTuningTarget for WhatsMinerV3 { } } impl GetScaledTuningTarget for WhatsMinerV3 {} -impl GetDefaultPowerTarget for WhatsMinerV3 {} -impl GetMinPowerTarget for WhatsMinerV3 {} -impl GetMaxPowerTarget for WhatsMinerV3 {} +impl GetTuningCapabilities for WhatsMinerV3 {} impl GetLightFlashing for WhatsMinerV3 { fn parse_light_flashing(&self, data: &HashMap) -> Option { data.extract_map::(DataField::LightFlashing, |l| l != "auto") diff --git a/python/pyasic_rs/asic_rs.pyi b/python/pyasic_rs/asic_rs.pyi index 474f24eb..c9308a7b 100644 --- a/python/pyasic_rs/asic_rs.pyi +++ b/python/pyasic_rs/asic_rs.pyi @@ -82,7 +82,6 @@ class DataField: AverageTemperature: Final[DataField] Chips: Final[DataField] ControlBoardVersion: Final[DataField] - DefaultPowerTarget: Final[DataField] DeviceInfo: Final[DataField] Efficiency: Final[DataField] ExpectedHashrate: Final[DataField] @@ -96,15 +95,14 @@ class DataField: IsMining: Final[DataField] LightFlashing: Final[DataField] Mac: Final[DataField] - MaxPowerTarget: Final[DataField] Messages: Final[DataField] - MinPowerTarget: Final[DataField] OutletFluidTemperature: Final[DataField] Pools: Final[DataField] PsuFans: Final[DataField] SchemaVersion: Final[DataField] SerialNumber: Final[DataField] Timestamp: Final[DataField] + TuningCapabilities: Final[DataField] TuningTarget: Final[DataField] Uptime: Final[DataField] Wattage: Final[DataField] @@ -498,8 +496,6 @@ class MinerData: @property def control_board_version(self, /) -> MinerControlBoard |None: ... @property - def default_power_target(self, /) -> float |None: ... - @property def device_info(self, /) -> DeviceInfo: ... @property def efficiency(self, /) -> float |None: ... @@ -532,11 +528,7 @@ class MinerData: @property def mac(self, /) -> str |None: ... @property - def max_power_target(self, /) -> float |None: ... - @property def messages(self, /) -> list[MinerMessage]: ... - @property - def min_power_target(self, /) -> float |None: ... def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... @classmethod def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... @@ -559,6 +551,8 @@ class MinerData: @property def total_chips(self, /) -> int |None: ... @property + def tuning_capabilities(self, /) -> TuningCapabilities |None: ... + @property def tuning_target(self, /) -> TuningTarget |None: ... @property def uptime(self, /) -> timedelta |None: ... @@ -848,6 +842,88 @@ class TuningConfig: @property def variant(self, /) -> str: ... +@final +class PowerTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "PowerTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "PowerTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def maximum(self, /) -> TuningTarget |None: ... + @property + def minimum(self, /) -> TuningTarget |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "PowerTuningCapabilities": ... + +@final +class HashRateTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "HashRateTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "HashRateTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def maximum(self, /) -> TuningTarget |None: ... + @property + def minimum(self, /) -> TuningTarget |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "HashRateTuningCapabilities": ... + +@final +class PresetTuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "PresetTuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "PresetTuningCapabilities": ... + @property + def default(self, /) -> TuningTarget |None: ... + @property + def presets(self, /) -> list[TuningTarget]: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "PresetTuningCapabilities": ... + +@final +class TuningCapabilities: + @classmethod + def __get_pydantic_core_schema__(cls, /, _source_type: "object", _handler: "object") -> "object": ... + def __repr__(self, /) -> str: ... + @staticmethod + def _pydantic_serialize(value: "TuningCapabilities") -> "dict[str, object]": ... + @classmethod + def _pydantic_validate(cls, /, value: "object") -> "TuningCapabilities": ... + @property + def hashrate(self, /) -> HashRateTuningCapabilities |None: ... + def model_dump(self, /, **_kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_json_schema(cls, /, **kwargs: "object") -> "dict[str, object]": ... + @classmethod + def model_validate(cls, /, obj: "object", **_kwargs: "object") -> "TuningCapabilities": ... + @property + def power(self, /) -> PowerTuningCapabilities |None: ... + @property + def presets(self, /) -> PresetTuningCapabilities |None: ... + class TuningTarget: def __repr__(self, /) -> str: ... def __str__(self, /) -> str: ... diff --git a/python/pyasic_rs/data.py b/python/pyasic_rs/data.py index 8dd008ef..76b3687c 100644 --- a/python/pyasic_rs/data.py +++ b/python/pyasic_rs/data.py @@ -6,6 +6,12 @@ """ from pyasic_rs.asic_rs import BoardData, ChipData +from pyasic_rs.asic_rs import ( + HashRateTuningCapabilities, + PowerTuningCapabilities, + PresetTuningCapabilities, + TuningCapabilities, +) from pyasic_rs.asic_rs import DeviceInfo, MinerHardware from pyasic_rs.asic_rs import FanData from pyasic_rs.asic_rs import HashRate, HashRateUnit @@ -20,6 +26,10 @@ "BoardData", "ChipData", "DeviceInfo", + "HashRateTuningCapabilities", + "PowerTuningCapabilities", + "PresetTuningCapabilities", + "TuningCapabilities", "FanData", "HashRate", "HashRateUnit", diff --git a/python/tests/test_pydantic_interop.py b/python/tests/test_pydantic_interop.py index eac5d2da..3bc678eb 100644 --- a/python/tests/test_pydantic_interop.py +++ b/python/tests/test_pydantic_interop.py @@ -119,9 +119,7 @@ def minimal_miner_data(**overrides: object) -> dict[str, object]: "wattage": None, "tuning_target": None, "scaled_tuning_target": None, - "default_power_target": None, - "min_power_target": None, - "max_power_target": None, + "tuning_capabilities": None, "efficiency": None, "light_flashing": None, "messages": [], diff --git a/src/python/mod.rs b/src/python/mod.rs index 20d69031..6a6e40e6 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -37,6 +37,10 @@ mod asic_rs { #[pymodule_export] use asic_rs_core::data::{ board::{BoardData, ChipData, MinerControlBoard}, + capabilities::{ + HashRateTuningCapabilities, PowerTuningCapabilities, PresetTuningCapabilities, + TuningCapabilities, + }, device::{DeviceInfo, MinerHardware}, fan::FanData, message::{MessageSeverity, MinerComponent, MinerMessage}, From 3dbfe99846cb10a3d3831fc35e66ba07d2bcec65 Mon Sep 17 00:00:00 2001 From: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:20:07 +0000 Subject: [PATCH 4/4] feat(braiins): tuning capabilities across all BOS backends + drop default-guard Address review on #292: - drop the `power == default => None` guard; presence of a populated key already signals which tuning types a miner supports - shared helpers in backends/util.rs: - power_target_capabilities(): GraphQL `bosminer/metadata/autotuning/powerTarget` (power-only envelope), used by the GraphQL backends 21.09 / 25.03 / 25.05 - tuner_constraints_capabilities(): BOS+ REST GET /configuration/constraints -> tuner_constraints, mapping PowerConstraints (watts) and HashrateConstraints (TH/s) to power + hashrate envelopes, used by the REST backends 25.07 / 26.04 - 26.04 switched from the GraphQL power-target query to the REST constraints endpoint, and now also reports the hashrate envelope --- .../braiins/src/backends/util.rs | 69 +++++++++++++++++++ .../braiins/src/backends/v21_09/mod.rs | 38 +++++++++- .../braiins/src/backends/v25_03/mod.rs | 38 +++++++++- .../braiins/src/backends/v25_05/mod.rs | 38 +++++++++- .../braiins/src/backends/v25_07/mod.rs | 28 +++++++- .../braiins/src/backends/v26_04/mod.rs | 53 ++++---------- 6 files changed, 217 insertions(+), 47 deletions(-) diff --git a/asic-rs-firmwares/braiins/src/backends/util.rs b/asic-rs-firmwares/braiins/src/backends/util.rs index 2b6ee616..2a6619fa 100644 --- a/asic-rs-firmwares/braiins/src/backends/util.rs +++ b/asic-rs-firmwares/braiins/src/backends/util.rs @@ -1,8 +1,77 @@ +use asic_rs_core::data::capabilities::{ + HashRateTuningCapabilities, PowerTuningCapabilities, TuningCapabilities, +}; use asic_rs_core::data::hashrate::{HashRate, HashRateUnit}; use asic_rs_core::data::miner::TuningTarget; use measurements::Power; use serde_json::Value; +/// Build [`TuningCapabilities`] from a BOS GraphQL `powerTarget` metadata object +/// (`{ default, min, max }`, all in watts). Older GraphQL BraiinsOS backends only +/// expose the power-target envelope this way. +pub(crate) fn power_target_capabilities(power_target: &Value) -> TuningCapabilities { + let watts = |key: &str| { + power_target + .get(key) + .and_then(|v| v.as_i64()) + .map(|w| TuningTarget::from_watts(w as f64)) + }; + TuningCapabilities { + power: Some(PowerTuningCapabilities { + default: watts("default"), + minimum: watts("min"), + maximum: watts("max"), + }), + ..Default::default() + } +} + +/// Build [`TuningCapabilities`] from a BOS+ REST `tuner_constraints` object +/// (`power_target` as `PowerConstraints` in watts, `hashrate_target` as +/// `HashrateConstraints` in TH/s). Used by the REST BraiinsOS backends +/// (25.07 and newer). +pub(crate) fn tuner_constraints_capabilities(tuner: &Value) -> TuningCapabilities { + let power = tuner.get("power_target").map(|target| { + let watts = |key: &str| { + target + .pointer(&format!("/{key}/watt")) + .and_then(|v| v.as_i64()) + .map(|w| TuningTarget::from_watts(w as f64)) + }; + PowerTuningCapabilities { + default: watts("default"), + minimum: watts("min"), + maximum: watts("max"), + } + }); + + let hashrate = tuner.get("hashrate_target").map(|target| { + let ths = |key: &str| { + target + .pointer(&format!("/{key}/terahash_per_second")) + .and_then(|v| v.as_f64()) + .map(|value| { + TuningTarget::HashRate(HashRate { + value, + unit: HashRateUnit::TeraHash, + algo: "SHA256".to_string(), + }) + }) + }; + HashRateTuningCapabilities { + default: ths("default"), + minimum: ths("min"), + maximum: ths("max"), + } + }); + + TuningCapabilities { + power, + hashrate, + presets: None, + } +} + pub(crate) fn parse_configured_tuning_target(value: &Value) -> Option { parse_tagged_tuning_target(value, "configured") } diff --git a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs index f29ca012..6625f696 100644 --- a/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v21_09/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -27,7 +28,9 @@ use measurements::{AngularVelocity, Frequency, Power, Temperature, Voltage}; use serde_json::{Value, json}; use web::BraiinsWebAPI; -use crate::backends::util::{parse_configured_tuning_target, parse_scaled_tuning_target}; +use crate::backends::util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, +}; use crate::{ backends::v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI}, @@ -196,6 +199,21 @@ impl GetDataLocations for BraiinsV2109 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -344,6 +362,14 @@ impl GetDataLocations for BraiinsV2109 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -394,7 +420,15 @@ impl GetFirmwareVersion for BraiinsV2109 { } impl GetControlBoardVersion for BraiinsV2109 {} -impl GetTuningCapabilities for BraiinsV2109 {} +impl GetTuningCapabilities for BraiinsV2109 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2109 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs index 810bd3ee..73007ba0 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_03/mod.rs @@ -2,7 +2,9 @@ use std::{collections::HashMap, net::IpAddr, str::FromStr, time::Duration}; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, + }, v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI, web::BraiinsWebAPI}, }, firmware::BraiinsFirmware, @@ -15,6 +17,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -191,6 +194,21 @@ impl GetDataLocations for BraiinsV2503 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -339,6 +357,14 @@ impl GetDataLocations for BraiinsV2503 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -389,7 +415,15 @@ impl GetFirmwareVersion for BraiinsV2503 { } impl GetControlBoardVersion for BraiinsV2503 {} -impl GetTuningCapabilities for BraiinsV2503 {} +impl GetTuningCapabilities for BraiinsV2503 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2503 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs index d562b06e..5a42550d 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_05/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::BoardData, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -28,7 +29,9 @@ use serde_json::{Value, json}; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, power_target_capabilities, + }, v21_09::{graphql::BraiinsGraphQLAPI, rpc::BraiinsRPCAPI, web::BraiinsWebAPI}, }, firmware::BraiinsFirmware, @@ -192,6 +195,21 @@ impl GetDataLocations for BraiinsV2505 { command: "admin/network/iface_status/lan", parameters: None, }; + const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { + command: r#"{ + bosminer { + metadata { + autotuning { + powerTarget { + default + min + max + } + } + } + } + }"#, + }; match data_field { DataField::ApiVersion => vec![( @@ -340,6 +358,14 @@ impl GetDataLocations for BraiinsV2505 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + GQL_POWER_TARGET_META_QUERY, + DataExtractor { + func: get_by_pointer, + key: Some("/bosminer/metadata/autotuning/powerTarget"), + tag: None, + }, + )], _ => vec![], } } @@ -390,7 +416,15 @@ impl GetFirmwareVersion for BraiinsV2505 { } impl GetControlBoardVersion for BraiinsV2505 {} -impl GetTuningCapabilities for BraiinsV2505 {} +impl GetTuningCapabilities for BraiinsV2505 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let power_target = data.get(&DataField::TuningCapabilities)?; + Some(power_target_capabilities(power_target)) + } +} impl GetHashboards for BraiinsV2505 { fn parse_hashboards(&self, data: &HashMap) -> Vec { diff --git a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs index 67b5f025..7a374ef5 100644 --- a/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v25_07/mod.rs @@ -8,6 +8,7 @@ use asic_rs_core::{ }, data::{ board::{BoardData, MinerControlBoard}, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -32,7 +33,10 @@ use web::BraiinsWebAPI; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, + tuner_constraints_capabilities, + }, v21_09::graphql::BraiinsGraphQLAPI, }, firmware::BraiinsFirmware, @@ -138,6 +142,10 @@ impl GetDataLocations for BraiinsV2507 { } }"#, }; + const WEB_CONSTRAINTS: MinerCommand = MinerCommand::WebAPI { + command: "configuration/constraints", + parameters: None, + }; match data_field { DataField::Mac => vec![( @@ -312,6 +320,14 @@ impl GetDataLocations for BraiinsV2507 { tag: None, }, )], + DataField::TuningCapabilities => vec![( + WEB_CONSTRAINTS, + DataExtractor { + func: get_by_pointer, + key: Some("/tuner_constraints"), + tag: None, + }, + )], _ => vec![], } } @@ -610,7 +626,15 @@ impl GetScaledTuningTarget for BraiinsV2507 { } impl GetFluidTemperature for BraiinsV2507 {} -impl GetTuningCapabilities for BraiinsV2507 {} +impl GetTuningCapabilities for BraiinsV2507 { + fn parse_tuning_capabilities( + &self, + data: &HashMap, + ) -> Option { + let tuner = data.get(&DataField::TuningCapabilities)?; + Some(tuner_constraints_capabilities(tuner)) + } +} impl GetPsuFans for BraiinsV2507 {} diff --git a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs index 687151cc..b4b5ce60 100644 --- a/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs +++ b/asic-rs-firmwares/braiins/src/backends/v26_04/mod.rs @@ -8,7 +8,7 @@ use asic_rs_core::{ }, data::{ board::{BoardData, MinerControlBoard}, - capabilities::{PowerTuningCapabilities, TuningCapabilities}, + capabilities::TuningCapabilities, collector::{ DataCollector, DataExtensions, DataExtractor, DataField, DataLocation, get_by_pointer, }, @@ -34,7 +34,10 @@ use web::BraiinsWebAPI; use crate::{ backends::{ - util::{parse_configured_tuning_target, parse_scaled_tuning_target}, + util::{ + parse_configured_tuning_target, parse_scaled_tuning_target, + tuner_constraints_capabilities, + }, v21_09::graphql::BraiinsGraphQLAPI, }, firmware::BraiinsFirmware, @@ -140,20 +143,9 @@ impl GetDataLocations for BraiinsV2604 { } }"#, }; - const GQL_POWER_TARGET_META_QUERY: MinerCommand = MinerCommand::GraphQL { - command: r#"{ - bosminer { - metadata { - autotuning { - powerTarget { - default - min - max - } - } - } - } - }"#, + const WEB_CONSTRAINTS: MinerCommand = MinerCommand::WebAPI { + command: "configuration/constraints", + parameters: None, }; match data_field { @@ -330,10 +322,10 @@ impl GetDataLocations for BraiinsV2604 { }, )], DataField::TuningCapabilities => vec![( - GQL_POWER_TARGET_META_QUERY, + WEB_CONSTRAINTS, DataExtractor { func: get_by_pointer, - key: Some("/bosminer/metadata/autotuning/powerTarget"), + key: Some("/tuner_constraints"), tag: None, }, )], @@ -635,27 +627,10 @@ impl GetTuningCapabilities for BraiinsV2604 { &self, data: &HashMap, ) -> Option { - // The extractor stores the `powerTarget` object ({default, min, max}) in - // watts under the field; read the three watt values from it. - let power_target = data.get(&DataField::TuningCapabilities)?; - let watts = |key: &str| { - power_target - .get(key) - .and_then(|v| v.as_i64()) - .map(|w| TuningTarget::from_watts(w as f64)) - }; - let power = PowerTuningCapabilities { - default: watts("default"), - minimum: watts("min"), - maximum: watts("max"), - }; - if power == PowerTuningCapabilities::default() { - return None; - } - Some(TuningCapabilities { - power: Some(power), - ..Default::default() - }) + // BOS+ REST `/configuration/constraints` → `tuner_constraints`, with + // power_target (watts) and hashrate_target (TH/s) envelopes. + let tuner = data.get(&DataField::TuningCapabilities)?; + Some(tuner_constraints_capabilities(tuner)) } }