//! Implement `get.device.info` command
//!
//! It is used to retrieve [DeviceInfo] about the ASIC device.
//!
//! - Command: [GetDeviceInfo]
//! - ApiDoc: <https://apidoc.whatsminer.com/#api-Device-device_get_info>
use serde::{Deserialize, Serialize};
mod error_code;
pub use error_code::*;
use crate::{
command::{Command, CommandLevel},
error::Result,
permission::PermissionLevel,
redacted::Redacted,
};
/// This command represents the `get.device.info` operation.
///
/// It is used to retrieve [DeviceInfo] about the ASIC device.
///
/// - ApiDoc: <https://apidoc.whatsminer.com/#api-Device-device_get_info>
///
/// # Example
/// ```rust,no_run
/// use matroskin::actor::Actor;
/// use matroskin::account::Account;
/// use matroskin::password::Password;
/// use matroskin::command::get_device_info::GetDeviceInfo;
///
/// #[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()?;
///
/// // Get all device info (default behavior)
/// let command = GetDeviceInfo::default();
/// let response = actor.send(command).await?;
/// println!("All Device Info: {:#?}", response);
///
/// Ok(())
/// }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GetDeviceInfo {
/// Include miner (ASIC) information
pub miner: bool,
/// Include power information
pub power: bool,
/// Include network information
pub network: bool,
/// Include system information
pub system: bool,
/// Include error code information
pub error_code: bool,
/// Include salt information
pub salt: bool,
}
impl Default for GetDeviceInfo {
fn default() -> Self {
Self {
miner: true,
power: true,
network: true,
system: true,
error_code: true,
salt: true,
}
}
}
impl GetDeviceInfo {
/// Create a new `get.device.info` command with specified section filters.
pub const fn new(
miner: bool,
power: bool,
network: bool,
system: bool,
error_code: bool,
salt: bool,
) -> Self {
Self {
miner,
power,
network,
system,
error_code,
salt,
}
}
/// Create a `get.device.info` command requesting all sections (default).
pub fn all() -> Self {
Self::default()
}
}
impl Command for GetDeviceInfo {
type Response = DeviceInfo;
const CMD_NAME: &'static str = "get.device.info";
const LEVEL: CommandLevel = CommandLevel::Public;
const MINIMAL_PERMISSIONS: PermissionLevel = PermissionLevel::ReadOnly;
fn params(&self) -> Result<Option<impl Serialize>> {
if self.miner && self.error_code && self.network && self.power && self.salt && self.system
|| !self.miner
&& !self.error_code
&& !self.network
&& !self.power
&& !self.salt
&& !self.system
{
Ok(None::<String>)
} else {
let mut out = Vec::with_capacity(6);
if self.miner {
out.push("miner");
}
if self.error_code {
out.push("error-code");
}
if self.network {
out.push("network");
}
if self.power {
out.push("power");
}
if self.salt {
out.push("salt");
}
if self.system {
out.push("system");
}
Ok(Some(out.join(",")))
}
}
}
/// Response structure containing various information about the ASIC device.
///
/// - ApiDoc: <https://apidoc.whatsminer.com/#api-Device-device_get_info>
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeviceInfo {
/// Network information
pub network: Option<Network>,
/// Miner information
pub miner: Option<Miner>,
/// System information
pub system: Option<System>,
/// Power information
pub power: Option<Power>,
/// Salt value
pub salt: Option<Redacted<String>>,
/// Error codes reported by the device.
///
/// - List of errors: <https://www.whatsminer.com/src/views/firmware-download.html#Document>
#[serde(rename = "error-code")]
pub error_codes: Option<Vec<DeviceErrorEntry>>,
}
/// Network information
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Network {
/// IP address
pub ip: String,
/// Protocol
pub proto: String,
/// Netmask
pub netmask: String,
/// DNS server
pub dns: String,
/// MAC address
pub mac: String,
/// Gateway address
pub gateway: String,
/// Hostname
pub hostname: String,
}
/// Miner information
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Miner {
/// Working status
#[serde(deserialize_with = "crate::response::deserialize_bool_from_string")]
pub working: bool,
/// Miner type
#[serde(rename = "type")]
pub r#type: String,
/// Hash board type
pub hash_board: String,
/// Detected hash rate
pub detect_hash_rate: String,
// TODO: make type, cause it's static
/// Coin type
pub cointype: String,
/// Pool strategy
pub pool_strategy: String,
/// Heat mode
pub heatmode: String,
/// Hash percent
pub hash_percent: String,
/// Liquid cooling status information (present if device supports liquid cooling)
pub eeprom_liquid_cooling: Option<String>,
/// Chip data 0
pub chipdata0: String,
/// Chip data 1
pub chipdata1: String,
/// Chip data 2
pub chipdata2: String,
/// Fast boot status
#[serde(deserialize_with = "crate::response::deserialize_bool_from_string")]
pub fast_boot: bool,
/// Board number
pub board_num: String,
/// PCB serial number 0
pub pcbsn0: String,
/// PCB serial number 1
pub pcbsn1: String,
/// PCB serial number 2
pub pcbsn2: String,
/// Miner serial number
pub miner_sn: String,
/// Power limit set
pub power_limit_set: String,
/// Web pool
pub web_pool: u32,
#[serde(rename = "UpfreqSpeed")]
/// Frequency upscaling speed setting
pub upfreq_speed: Option<String>,
/// Current permission level string (e.g. 'super=255 user1=0 user2=0 user3=0')
pub permission: Option<String>,
}
/// System information
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct System {
/// API version
pub api: String,
/// Platform
pub platform: String,
/// Firmware version
pub fwversion: String,
/// Control board version
pub control_board_version: String,
/// Boot ROM version
pub btrom: Option<String>,
/// API switch status: '0' (disabled), '1' (enabled)
pub apiswitch: String,
/// LED status
pub ledstatus: String,
}
/// Power supply information
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Power {
#[serde(rename = "type")]
/// Power supply type
pub r#type: String,
/// Power supply mode
pub mode: String,
/// Hardware version
pub hwversion: String,
/// Software version
pub swversion: String,
/// Model name
pub model: String,
/// Input current (A)
pub iin: f32,
/// Input voltage (V)
pub vin: f32,
/// Output voltage (10mV unit), e.g., value 1222 ≈ 12.22V
pub vout: i32,
/// Output power (W)
pub pin: i32,
/// Fan speed (RPM)
pub fanspeed: i32,
/// Temperature (°C)
pub temp0: f32,
/// Serial number
pub sn: String,
/// Vendor ID
pub vendor: String,
}
#[cfg(test)]
mod tests {
use super::*;
const RESPONSE_JSON: &str = r#"{"code":0,"when":1761111204,"msg":{"network":{"ip":"10.10.10.1","proto":"dhcp","netmask":"255.255.255.0","dns":"10.10.10.1","mac":"AA:BB:CC:DD:EE:FF","gateway":"10.10.10.1","hostname":"WhatsMiner"},"miner":{"working":"true","type":"M50_VH84","hash-board":"H84","detect-hash-rate":"40000:41000:40000","cointype":"BTC/BCH/BSV","pool-strategy":"FAILOVER","heatmode":"","hash-percent":"","chipdata0":"H3LA03-XXXXXXXX BINVE2-XXXXXXX","chipdata1":"H3LA03-XXXXXXXX BINVE2-XXXXXXX","chipdata2":"H3LA03-XXXXXXXX BINVE2-XXXXXXX","fast-boot":"disable","board-num":"3","pcbsn0":"HHM00X0F000000X00000","pcbsn1":"HHM00X0F000000X00000","pcbsn2":"HHM00X0F000000X00000","miner-sn":"HTM00X00KU0000000000000A00000","power-limit-set":"","UpfreqSpeed":"","web-pool":1,"permission":"super=255 user1=0 user2=0 user3=0"},"system":{"api":"3.0.2","platform":"H616","fwversion":"20250915.16.Rel2","control-board-version":"CB6V10","apiswitch":"1","ledstatus":"auto"},"power":{"type":"P221B","mode":"1","hwversion":"V00.00","swversion":"V02.00.V02.00","model":"P221B","iin":15.43,"vin":231.5,"vout":1222,"pin":3573,"fanspeed":4992,"temp0":79,"sn":"A0000G000000000000","vendor":"7"},"salt":"TESTSALT","error-code":[]},"desc":"get.device.info"}"#;
#[test]
fn request_default_all_params() {
let cmd = GetDeviceInfo::default();
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.device.info");
// default = all true = no param sent
assert!(json.get("param").is_none());
}
#[test]
fn request_partial_params() {
let cmd = GetDeviceInfo {
network: true,
system: true,
miner: false,
power: false,
salt: false,
error_code: false,
};
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.device.info");
assert_eq!(json["param"], "network,system");
}
#[test]
fn request_only_miner_param() {
let cmd = GetDeviceInfo {
miner: true,
network: false,
system: false,
power: false,
salt: false,
error_code: false,
};
let encoded = cmd.encode(None).unwrap();
let json: serde_json::Value = serde_json::from_slice(&encoded.0).unwrap();
assert_eq!(json["cmd"], "get.device.info");
assert_eq!(json["param"], "miner");
}
#[test]
fn deserialize_response_fixture() {
let response = GetDeviceInfo::parse_response(RESPONSE_JSON.as_bytes()).unwrap();
assert!(response.code.is_success());
assert_eq!(response.desc, "get.device.info");
assert_eq!(response.when, 1761111204);
let info = &response.msg;
// Network
let net = info.network.as_ref().unwrap();
assert_eq!(net.proto, "dhcp");
assert_eq!(net.hostname, "WhatsMiner");
// Miner
let miner = info.miner.as_ref().unwrap();
assert_eq!(miner.r#type, "M50_VH84");
assert!(miner.working);
assert!(!miner.fast_boot);
assert_eq!(miner.board_num, "3");
assert_eq!(miner.web_pool, 1);
// System
let sys = info.system.as_ref().unwrap();
assert_eq!(sys.api, "3.0.2");
assert_eq!(sys.platform, "H616");
assert_eq!(sys.fwversion, "20250915.16.Rel2");
// Power
let pwr = info.power.as_ref().unwrap();
assert_eq!(pwr.r#type, "P221B");
assert_eq!(pwr.iin, 15.43);
assert_eq!(pwr.vin, 231.5);
assert_eq!(pwr.vout, 1222);
assert_eq!(pwr.pin, 3573);
// Salt
assert_eq!(info.salt.as_ref().map(|s| s.as_str()), Some("TESTSALT"));
// Error codes — fixed with #[serde(rename = "error-code")]
assert_eq!(info.error_codes.as_ref().map(|v| v.len()), Some(0));
}
}