matroskin 0.1.0

whatsminer api client lib
Documentation
//! 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));
    }
}