//! Implement `get.miner.status` command
//!
//! This command is used to get the miner status summary, pool information,
//! and/or device (edevs) information.
//!
//! - Command: [GetMinerStatus]
//! - ApiDoc: <https://apidoc.whatsminer.com/#api-Miner-btminer_get_status>
use core::str;
use serde::Deserialize;
use crate::{
command::{Command, CommandLevel},
error::Result,
permission::PermissionLevel,
};
/// This command represents the `get.miner.status` operation.
///
/// It is used to get the miner status summary, pool information,
/// and/or device (edevs) information.
///
/// The `param` field controls which sections are returned. If no param
/// flags are set, the API returns no data (empty `msg`).
///
/// - ApiDoc: <https://apidoc.whatsminer.com/#api-Miner-btminer_get_status>
///
/// # Example
/// ```rust,no_run
/// use matroskin::actor::Actor;
/// use matroskin::command::get_miner_status::GetMinerStatus;
/// use matroskin::account::Account;
/// use matroskin::password::Password;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let addr = "10.10.10.10:4433";
/// let username = Account::Super;
/// let password = Password::Super;
///
/// let actor = Actor::new(addr, username, password);
/// actor.connect()?;
///
/// // Request all status sections (summary, pools, edevs)
/// let response = actor.send(GetMinerStatus::all()).await?;
/// println!("Status: {:#?}", response);
///
/// Ok(())
/// }
/// ```
/// This command represents the `get.miner.status` operation.
///
/// Each flag controls whether the corresponding section is included
/// in the response. Flags are combined with `+` in the request param
/// string (e.g. `"summary+pools"`).
///
/// - ApiDoc: <https://apidoc.whatsminer.com/#api-Miner-btminer_get_status>
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct GetMinerStatus {
/// Include miner status summary
pub summary: bool,
/// Include pool information
pub pools: bool,
/// Include device (edevs) information
pub edevs: bool,
}
impl GetMinerStatus {
/// Create a new `get.miner.status` command with explicit flags.
pub fn new(summary: bool, pools: bool, edevs: bool) -> Self {
Self {
summary,
pools,
edevs,
}
}
/// Request miner status summary only.
pub fn summary() -> Self {
Self {
summary: true,
pools: false,
edevs: false,
}
}
/// Request pool information only.
pub fn pools() -> Self {
Self {
summary: false,
pools: true,
edevs: false,
}
}
/// Request device (edevs) information only.
pub fn edevs() -> Self {
Self {
summary: false,
pools: false,
edevs: true,
}
}
/// Request all sections (summary, pools, edevs).
pub fn all() -> Self {
Self {
summary: true,
pools: true,
edevs: true,
}
}
}
/// [GetMinerStatus] Response
///
/// Only the sections requested via [GetMinerStatus] will be
/// populated; the others will be `None`.
#[derive(Debug, Default, Deserialize)]
pub struct GetMinerStatusResponse {
/// Miner status summary (present when `summary` param is set)
#[serde(default)]
pub summary: Option<MinerStatusSummary>,
/// Pool information list (present when `pools` param is set)
#[serde(default)]
pub pools: Option<Vec<MinerStatusPool>>,
/// Device information list (present when `edevs` param is set)
#[serde(default)]
pub edevs: Option<Vec<MinerStatusEdev>>,
}
/// Miner status summary returned by `get.miner.status` with `summary` param.
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MinerStatusSummary {
/// Elapsed time since boot (seconds)
pub elapsed: i64,
/// Boot-up time (seconds)
pub bootup_time: i64,
/// Average frequency across all boards
pub freq_avg: i64,
/// Target frequency (0 = not set)
pub target_freq: i64,
/// Factory-calibrated hash rate (TH/s)
pub factory_hash: f64,
/// Average hash rate (TH/s)
pub hash_average: f64,
/// 1-minute hash rate (TH/s)
pub hash_1min: f64,
/// 15-minute hash rate (TH/s)
pub hash_15min: f64,
/// Real-time hash rate (TH/s)
pub hash_realtime: f64,
/// Power consumption rate (W/TH)
pub power_rate: f64,
/// 5-minute average power consumption (W)
pub power_5min: f64,
/// Real-time power consumption (W)
pub power_realtime: f64,
/// Environment temperature (°C)
pub environment_temperature: f64,
/// Board temperatures (°C), one entry per hash board
pub board_temperature: Vec<f64>,
/// Minimum chip temperature across all boards (°C)
pub chip_temp_min: f64,
/// Average chip temperature across all boards (°C)
pub chip_temp_avg: f64,
/// Maximum chip temperature across all boards (°C)
pub chip_temp_max: f64,
/// Power limit (W)
pub power_limit: i64,
/// Whether frequency upscaling is finished (1 = yes)
pub up_freq_finish: i64,
/// Intake fan speed (RPM)
pub fan_speed_in: i64,
/// Exhaust fan speed (RPM)
pub fan_speed_out: i64,
}
/// Pool information returned by `get.miner.status` with `pools` param.
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MinerStatusPool {
/// Pool ID
pub id: i64,
/// Pool URL (e.g. `stratum+tcp://...`)
pub url: String,
/// Pool connection status (e.g. `"alive"`)
pub status: String,
/// Pool account/worker name
pub account: String,
/// Whether this is the active stratum connection
pub stratum_active: bool,
/// Share reject rate (0–1)
pub reject_rate: f64,
/// UNIX timestamp of the last accepted share
pub last_share_time: i64,
}
/// Device information returned by `get.miner.status` with `edevs` param.
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct MinerStatusEdev {
/// Device ID
pub id: i64,
/// Slot index
pub slot: i64,
/// Average hash rate for this device (TH/s)
pub hash_average: f64,
/// Factory-calibrated hash rate for this device (TH/s)
pub factory_hash: f64,
/// Frequency for this device (MHz)
pub freq: i64,
/// Number of effective (working) chips
pub effective_chips: i64,
/// Minimum chip temperature on this device (°C)
pub chip_temp_min: f64,
/// Average chip temperature on this device (°C)
pub chip_temp_avg: f64,
/// Maximum chip temperature on this device (°C)
pub chip_temp_max: f64,
}
impl Command for GetMinerStatus {
type Response = GetMinerStatusResponse;
const CMD_NAME: &'static str = "get.miner.status";
const LEVEL: CommandLevel = CommandLevel::Public;
const MINIMAL_PERMISSIONS: PermissionLevel = PermissionLevel::ReadOnly;
fn params(&self) -> Result<Option<impl serde::Serialize>> {
if !self.summary && !self.pools && !self.edevs {
return Ok(None);
}
let mut parts = Vec::with_capacity(3);
if self.summary {
parts.push("summary");
}
if self.pools {
parts.push("pools");
}
if self.edevs {
parts.push("edevs");
}
Ok(Some(parts.join("+")))
}
}
#[cfg(test)]
mod tests {
use super::*;
const SUMMARY_JSON: &str = r#"{"code":0,"when":1692685476,"msg":{"summary":{"elapsed":1074,"bootup-time":1175,"freq-avg":788,"target-freq":0,"factory-hash":101.366,"hash-average":101.847,"hash-1min":101.072,"hash-15min":101.847,"hash-realtime":101.847,"power-rate":31.886,"power-5min":3247.641,"power-realtime":3268,"environment-temperature":34.8,"board-temperature":[69.6,70.1,72.3],"chip-temp-min":83.2,"chip-temp-avg":92.9,"chip-temp-max":100.3,"power-limit":3500,"up-freq-finish":1,"fan-speed-in":4980,"fan-speed-out":5070}},"desc":"get.miner.status"}"#;
const POOLS_JSON: &str = r#"{"code":0,"when":1692685470,"msg":{"pools":[{"id":1,"url":"stratum+tcp://192.168.2.24:5353","status":"alive","account":"xxxxxxxx","stratum-active":true,"reject-rate":0,"last-share-time":1692685469}]},"desc":"get.miner.status"}"#;
const EDEVS_JSON: &str = r#"{"code":0,"when":1692685473,"msg":{"edevs":[{"id":0,"slot":0,"hash-average":33.972,"factory-hash":33.789,"freq":785,"effective-chips":70,"chip-temp-min":84.9,"chip-temp-avg":92.2,"chip-temp-max":97.4}]},"desc":"get.miner.status"}"#;
#[test]
fn request_no_params_returns_none() {
let cmd = GetMinerStatus::default();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert!(json.get("param").is_none());
assert!(json.get("token").is_none());
}
#[test]
fn request_summary_only() {
let cmd = GetMinerStatus::summary();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert_eq!(json["param"], "summary");
}
#[test]
fn request_pools_only() {
let cmd = GetMinerStatus::pools();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert_eq!(json["param"], "pools");
}
#[test]
fn request_edevs_only() {
let cmd = GetMinerStatus::edevs();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert_eq!(json["param"], "edevs");
}
#[test]
fn request_summary_plus_pools() {
let cmd = GetMinerStatus::new(true, true, false);
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert_eq!(json["param"], "summary+pools");
}
#[test]
fn request_all_params() {
let cmd = GetMinerStatus::all();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.miner.status");
assert_eq!(json["param"], "summary+pools+edevs");
}
#[test]
fn deserialize_summary_response_fixture() {
let response = GetMinerStatus::parse_response(SUMMARY_JSON.as_bytes()).unwrap();
assert!(response.code.is_success());
assert_eq!(response.desc, "get.miner.status");
assert_eq!(response.when, 1692685476);
let summary = response.msg.summary.unwrap();
assert_eq!(summary.elapsed, 1074);
assert_eq!(summary.bootup_time, 1175);
assert_eq!(summary.freq_avg, 788);
assert_eq!(summary.target_freq, 0);
assert!((summary.factory_hash - 101.366).abs() < f64::EPSILON);
assert!((summary.hash_average - 101.847).abs() < f64::EPSILON);
assert!((summary.hash_1min - 101.072).abs() < f64::EPSILON);
assert!((summary.hash_15min - 101.847).abs() < f64::EPSILON);
assert!((summary.hash_realtime - 101.847).abs() < f64::EPSILON);
assert!((summary.power_rate - 31.886).abs() < f64::EPSILON);
assert!((summary.power_5min - 3247.641).abs() < f64::EPSILON);
assert!((summary.power_realtime - 3268.0).abs() < f64::EPSILON);
assert!((summary.environment_temperature - 34.8).abs() < f64::EPSILON);
assert_eq!(summary.board_temperature, vec![69.6, 70.1, 72.3]);
assert!((summary.chip_temp_min - 83.2).abs() < f64::EPSILON);
assert!((summary.chip_temp_avg - 92.9).abs() < f64::EPSILON);
assert!((summary.chip_temp_max - 100.3).abs() < f64::EPSILON);
assert_eq!(summary.power_limit, 3500);
assert_eq!(summary.up_freq_finish, 1);
assert_eq!(summary.fan_speed_in, 4980);
assert_eq!(summary.fan_speed_out, 5070);
// No pools or edevs in this response
assert!(response.msg.pools.is_none());
assert!(response.msg.edevs.is_none());
}
#[test]
fn deserialize_pools_response_fixture() {
let response = GetMinerStatus::parse_response(POOLS_JSON.as_bytes()).unwrap();
assert!(response.code.is_success());
assert_eq!(response.desc, "get.miner.status");
assert_eq!(response.when, 1692685470);
let pools = response.msg.pools.unwrap();
assert_eq!(pools.len(), 1);
let pool = &pools[0];
assert_eq!(pool.id, 1);
assert_eq!(pool.url, "stratum+tcp://192.168.2.24:5353");
assert_eq!(pool.status, "alive");
assert_eq!(pool.account, "xxxxxxxx");
assert!(pool.stratum_active);
assert!((pool.reject_rate - 0.0).abs() < f64::EPSILON);
assert_eq!(pool.last_share_time, 1692685469);
// No summary or edevs in this response
assert!(response.msg.summary.is_none());
assert!(response.msg.edevs.is_none());
}
#[test]
fn deserialize_edevs_response_fixture() {
let response = GetMinerStatus::parse_response(EDEVS_JSON.as_bytes()).unwrap();
assert!(response.code.is_success());
assert_eq!(response.desc, "get.miner.status");
assert_eq!(response.when, 1692685473);
let edevs = response.msg.edevs.unwrap();
assert_eq!(edevs.len(), 1);
let edev = &edevs[0];
assert_eq!(edev.id, 0);
assert_eq!(edev.slot, 0);
assert!((edev.hash_average - 33.972).abs() < f64::EPSILON);
assert!((edev.factory_hash - 33.789).abs() < f64::EPSILON);
assert_eq!(edev.freq, 785);
assert_eq!(edev.effective_chips, 70);
assert!((edev.chip_temp_min - 84.9).abs() < f64::EPSILON);
assert!((edev.chip_temp_avg - 92.2).abs() < f64::EPSILON);
assert!((edev.chip_temp_max - 97.4).abs() < f64::EPSILON);
// No summary or pools in this response
assert!(response.msg.summary.is_none());
assert!(response.msg.pools.is_none());
}
}